完善可视化大屏的图表
This commit is contained in:
parent
f401f40a6e
commit
1265cf7553
33
src/api/monitor/alert.js
Normal file
33
src/api/monitor/alert.js
Normal file
@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取预警统计概览
|
||||
* @returns {Promise} 返回预警统计数据
|
||||
*/
|
||||
export function getAlertStatisticsOverview() {
|
||||
return request.get('/api/admin/alert/statistics/overview')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预警趋势统计
|
||||
* @returns {Promise} 返回预警趋势统计数据
|
||||
*/
|
||||
export function getAlertStatisticsTrend() {
|
||||
return request.get('/api/admin/alert/statistics/trend')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标预警统计
|
||||
* @returns {Promise} 返回各指标预警统计数据
|
||||
*/
|
||||
export function getAlertStatisticsByIndicator() {
|
||||
return request.get('/api/admin/alert/statistics/by-indicator')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则预警统计
|
||||
* @returns {Promise} 返回规则预警统计数据
|
||||
*/
|
||||
export function getAlertStatisticsByRule() {
|
||||
return request.get('/api/admin/alert/statistics/by-rule')
|
||||
}
|
||||
49
src/api/monitoring.js
Normal file
49
src/api/monitoring.js
Normal file
@ -0,0 +1,49 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 按时间范围查询监测数据
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.start_date - 开始时间
|
||||
* @param {string} params.end_date - 结束时间
|
||||
* @param {Array} params.indicator_ids - 指标ID数组
|
||||
* @param {number} params.point_id - 监测点ID
|
||||
* @param {number} params.device_id - 设备ID
|
||||
*/
|
||||
export function getMonitoringData(params) {
|
||||
return request.get('/api/monitoring/data', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新数据
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.point_id - 监测点ID
|
||||
* @param {Array} params.indicator_ids - 指标ID数组
|
||||
* @param {number} params.device_id - 设备ID
|
||||
*/
|
||||
export function getLatestData(params) {
|
||||
return request.get('/api/monitoring/data/latest', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计数据
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.start_date - 开始时间
|
||||
* @param {string} params.end_date - 结束时间
|
||||
* @param {number} params.indicator_id - 指标ID
|
||||
* @param {number} params.point_id - 监测点ID
|
||||
* @param {number} params.device_id - 设备ID
|
||||
*/
|
||||
export function getStatistics(params) {
|
||||
return request.get('/api/monitoring/data/statistics', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据质量统计
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.start_date - 开始时间
|
||||
* @param {string} params.end_date - 结束时间
|
||||
* @param {number} params.point_id - 监测点ID
|
||||
*/
|
||||
export function getQualityStatistics(params) {
|
||||
return request.get('/api/monitoring/data/quality-statistics', { params })
|
||||
}
|
||||
@ -1,13 +1,275 @@
|
||||
<script setup>
|
||||
// 后续添加具体实现
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import request from '@/utils/request'
|
||||
|
||||
let speciesChart = null
|
||||
let patrolChart = null
|
||||
const speciesChartRef = ref(null)
|
||||
const patrolChartRef = ref(null)
|
||||
|
||||
// 获取物种统计数据
|
||||
const fetchSpeciesData = async () => {
|
||||
try {
|
||||
const res = await request.get('/api/admin/species/statistics/overview')
|
||||
if (res.success && res.data) {
|
||||
updateSpeciesChart(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取物种统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取巡护统计数据
|
||||
const fetchPatrolData = async () => {
|
||||
try {
|
||||
const res = await request.get('/api/admin/patrol/records/statistics/overview')
|
||||
if (res.success && res.data) {
|
||||
updatePatrolChart(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取巡护统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化物种统计图表
|
||||
const initSpeciesChart = () => {
|
||||
if (!speciesChartRef.value) return
|
||||
|
||||
speciesChart = echarts.init(speciesChartRef.value)
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
title: {
|
||||
text: '物种分布统计',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 16
|
||||
},
|
||||
left: 'center',
|
||||
top: 0
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: '5%',
|
||||
top: 'middle',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['40%', '55%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
color: '#fff',
|
||||
formatter: '{b}\n{c}种'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: []
|
||||
}
|
||||
]
|
||||
}
|
||||
speciesChart.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化巡护统计图表
|
||||
const initPatrolChart = () => {
|
||||
if (!patrolChartRef.value) return
|
||||
|
||||
patrolChart = echarts.init(patrolChartRef.value)
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
title: {
|
||||
text: '巡护任务统计',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 16
|
||||
},
|
||||
left: 'center',
|
||||
top: 0
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: '15%',
|
||||
right: '5%',
|
||||
bottom: '5%',
|
||||
left: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['已完成', '进行中', '未开始', '已超时'],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)',
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: '40%',
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
color: '#fff'
|
||||
},
|
||||
data: []
|
||||
}
|
||||
]
|
||||
}
|
||||
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
|
||||
const startAutoRefresh = () => {
|
||||
fetchSpeciesData()
|
||||
fetchPatrolData()
|
||||
timer = setInterval(() => {
|
||||
fetchSpeciesData()
|
||||
fetchPatrolData()
|
||||
}, 60000) // 每分钟更新一次
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
speciesChart?.resize()
|
||||
patrolChart?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initSpeciesChart()
|
||||
initPatrolChart()
|
||||
startAutoRefresh()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (speciesChart) {
|
||||
speciesChart.dispose()
|
||||
speciesChart = null
|
||||
}
|
||||
if (patrolChart) {
|
||||
patrolChart.dispose()
|
||||
patrolChart = null
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="data-chart">
|
||||
<div class="chart-title">数据统计</div>
|
||||
<div class="chart-content">
|
||||
<!-- 临时占位内容 -->
|
||||
<div class="placeholder">数据图表内容区域</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-item">
|
||||
<div class="chart-title">物种分布统计</div>
|
||||
<div ref="speciesChartRef" class="species-chart"></div>
|
||||
</div>
|
||||
<div class="chart-item">
|
||||
<div class="chart-title">巡护任务统计</div>
|
||||
<div ref="patrolChartRef" class="patrol-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -16,22 +278,40 @@
|
||||
.data-chart {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(6, 30, 93, 0.5);
|
||||
border-radius: 4px;
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
height: calc(100% - 32px);
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
.placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
.chart-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
background: linear-gradient(to bottom, #ffffff, #3fa7dd);
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.species-chart,
|
||||
.patrol-chart {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,215 @@
|
||||
<script setup>
|
||||
// 后续添加具体实现
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getAlertStatisticsTrend } from '@/api/monitor/alert'
|
||||
|
||||
let chart = null
|
||||
const chartRef = ref(null)
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chart = echarts.init(chartRef.value)
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['严重', '中等', '轻微'],
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
top: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '25%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
rotate: 30
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '预警数量',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '严重',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 0
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(245, 108, 108, 0.8)' },
|
||||
{ offset: 1, color: 'rgba(245, 108, 108, 0.1)' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: []
|
||||
},
|
||||
{
|
||||
name: '中等',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 0
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(230, 162, 60, 0.8)' },
|
||||
{ offset: 1, color: 'rgba(230, 162, 60, 0.1)' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: []
|
||||
},
|
||||
{
|
||||
name: '轻微',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 0
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(103, 194, 58, 0.8)' },
|
||||
{ offset: 1, color: 'rgba(103, 194, 58, 0.1)' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
// 获取预警趋势数据
|
||||
const fetchTrendData = async () => {
|
||||
try {
|
||||
const res = await getAlertStatisticsTrend()
|
||||
if (res.success && res.data) {
|
||||
const { dates, severe, moderate, minor } = res.data
|
||||
|
||||
chart.setOption({
|
||||
xAxis: {
|
||||
data: dates
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '严重',
|
||||
data: severe
|
||||
},
|
||||
{
|
||||
name: '中等',
|
||||
data: moderate
|
||||
},
|
||||
{
|
||||
name: '轻微',
|
||||
data: minor
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预警趋势数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新数据
|
||||
let timer = null
|
||||
const startAutoRefresh = () => {
|
||||
fetchTrendData()
|
||||
timer = setInterval(fetchTrendData, 60000) // 每分钟更新一次
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chart?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
startAutoRefresh()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
chart = null
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bottom-card">
|
||||
<div class="card-title">预警信息</div>
|
||||
<div class="card-content">
|
||||
<!-- 临时占位内容 -->
|
||||
<div class="placeholder">预警信息内容区域</div>
|
||||
<div class="card-header">
|
||||
<div class="title">预警趋势</div>
|
||||
<div class="update-time">实时监测中</div>
|
||||
</div>
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -16,23 +217,56 @@
|
||||
.bottom-card {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(6, 30, 93, 0.5);
|
||||
border-radius: 4px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
height: calc(100% - 32px);
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: calc(100% - 60px);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.2; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.2; }
|
||||
}
|
||||
</style>
|
||||
@ -1,13 +1,304 @@
|
||||
<script setup>
|
||||
// 后续添加具体实现
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getMonitoringData, getLatestData, getStatistics, getQualityStatistics } from '@/api/monitoring'
|
||||
|
||||
// 图表实例
|
||||
let trendChart = null
|
||||
let qualityChart = null
|
||||
let indicatorChart = null
|
||||
|
||||
// 图表容器引用
|
||||
const trendChartRef = ref(null)
|
||||
const qualityChartRef = ref(null)
|
||||
const indicatorChartRef = ref(null)
|
||||
|
||||
// 指标选项
|
||||
const indicatorOptions = [
|
||||
{ label: '水温', unit: '°C', threshold: { min: 15, max: 30 } },
|
||||
{ label: 'pH值', unit: '', threshold: { min: 6.5, max: 8.5 } },
|
||||
{ label: '溶解氧', unit: 'mg/L', threshold: { min: 5, max: 9 } },
|
||||
{ label: '浊度', unit: 'NTU', threshold: { min: 0, max: 10 } }
|
||||
]
|
||||
|
||||
// 更新实时指标图表
|
||||
const updateIndicatorChart = async () => {
|
||||
if (!indicatorChart || !indicatorChartRef.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[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: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.map(item => item.name),
|
||||
axisLine: { lineStyle: { color: '#fff' } },
|
||||
axisLabel: { color: '#fff' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { lineStyle: { color: '#fff' } },
|
||||
axisLabel: { color: '#fff' },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: data,
|
||||
barWidth: '40%',
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
color: '#fff',
|
||||
formatter: (params) => {
|
||||
const indicator = indicatorOptions[params.dataIndex]
|
||||
return params.value + (indicator.unit || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取最新数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新趋势图表
|
||||
const updateTrendChart = async () => {
|
||||
if (!trendChart || !trendChartRef.value) return
|
||||
|
||||
try {
|
||||
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 = () => {
|
||||
indicatorChart?.resize()
|
||||
trendChart?.resize()
|
||||
qualityChart?.resize()
|
||||
}
|
||||
|
||||
// 定时更新数据
|
||||
let updateTimer = null
|
||||
const startDataUpdate = () => {
|
||||
updateIndicatorChart()
|
||||
updateTimer = setInterval(() => {
|
||||
updateIndicatorChart()
|
||||
}, 60000) // 每分钟更新一次
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCharts()
|
||||
startDataUpdate()
|
||||
updateTrendChart()
|
||||
updateQualityChart()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (updateTimer) {
|
||||
clearInterval(updateTimer)
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
indicatorChart?.dispose()
|
||||
trendChart?.dispose()
|
||||
qualityChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="middle-card">
|
||||
<div class="card-title">监测数据</div>
|
||||
<div class="card-content">
|
||||
<!-- 临时占位内容 -->
|
||||
<div class="placeholder">监测数据内容区域</div>
|
||||
<!-- 实时指标图表 -->
|
||||
<div ref="indicatorChartRef" class="indicator-chart"></div>
|
||||
|
||||
<!-- 趋势图表 -->
|
||||
<div ref="trendChartRef" class="trend-chart"></div>
|
||||
|
||||
<!-- 数据质量图表 -->
|
||||
<div ref="qualityChartRef" class="quality-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -16,6 +307,8 @@
|
||||
.middle-card {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
@ -27,11 +320,22 @@
|
||||
.card-content {
|
||||
height: calc(100% - 32px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
.indicator-chart {
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.trend-chart {
|
||||
flex: 2;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.quality-chart {
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,39 +1,56 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getAlertStatisticsOverview } from '@/api/monitor/alert'
|
||||
|
||||
// 生态指标模拟数据
|
||||
const ecoData = ref({
|
||||
airQuality: {
|
||||
name: '空气质量指数',
|
||||
value: [65, 68, 75, 82, 86, 82, 78],
|
||||
level: '良好',
|
||||
color: '#67C23A'
|
||||
// 预警数据
|
||||
const alertData = ref({
|
||||
water_quality: {
|
||||
name: '水质预警',
|
||||
value: [],
|
||||
level: '正常',
|
||||
color: '#67C23A',
|
||||
count: 0
|
||||
},
|
||||
waterQuality: {
|
||||
name: '水质指数',
|
||||
value: [92, 90, 94, 95, 93, 96, 95],
|
||||
level: '优',
|
||||
color: '#409EFF'
|
||||
air_quality: {
|
||||
name: '空气质量预警',
|
||||
value: [],
|
||||
level: '正常',
|
||||
color: '#409EFF',
|
||||
count: 0
|
||||
},
|
||||
biodiversity: {
|
||||
name: '生物多样性',
|
||||
value: [72, 75, 73, 78, 76, 74, 78],
|
||||
level: '中等',
|
||||
color: '#E6A23C'
|
||||
ecosystem: {
|
||||
name: '生态系统预警',
|
||||
value: [],
|
||||
level: '正常',
|
||||
color: '#E6A23C',
|
||||
count: 0
|
||||
},
|
||||
vegetation: {
|
||||
name: '植被覆盖率',
|
||||
value: [82, 80, 85, 83, 85, 87, 85],
|
||||
level: '良好',
|
||||
color: '#67C23A'
|
||||
weather: {
|
||||
name: '气象预警',
|
||||
value: [],
|
||||
level: '正常',
|
||||
color: '#909399',
|
||||
count: 0
|
||||
}
|
||||
})
|
||||
|
||||
const timeData = ['10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00']
|
||||
const timeData = ref([])
|
||||
|
||||
let chart = null
|
||||
|
||||
// 初始化时间数据
|
||||
const initTimeData = () => {
|
||||
const now = new Date()
|
||||
const times = []
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const time = new Date(now - i * 3600 * 1000)
|
||||
times.push(time.getHours() + ':00')
|
||||
}
|
||||
timeData.value = times
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
const chartDom = document.getElementById('ecoChart')
|
||||
if (!chartDom) return
|
||||
@ -49,7 +66,7 @@ const initChart = () => {
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['空气质量指数', '水质指数', '生物多样性', '植被覆盖率'],
|
||||
data: Object.values(alertData.value).map(item => item.name),
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
@ -64,7 +81,7 @@ const initChart = () => {
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: timeData,
|
||||
data: timeData.value,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.3)'
|
||||
@ -76,6 +93,7 @@ const initChart = () => {
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '预警次数',
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
@ -90,114 +108,95 @@ const initChart = () => {
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '空气质量指数',
|
||||
type: 'bar',
|
||||
data: ecoData.value.airQuality.value,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#67C23A' },
|
||||
{ offset: 1, color: 'rgba(103, 194, 58, 0.3)' }
|
||||
])
|
||||
},
|
||||
barWidth: '15%'
|
||||
series: Object.values(alertData.value).map(item => ({
|
||||
name: item.name,
|
||||
type: 'line',
|
||||
data: item.value,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: item.color
|
||||
},
|
||||
{
|
||||
name: '水质指数',
|
||||
type: 'line',
|
||||
data: ecoData.value.waterQuality.value,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: '#409EFF'
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#409EFF',
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}
|
||||
itemStyle: {
|
||||
color: item.color,
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
},
|
||||
{
|
||||
name: '生物多样性',
|
||||
type: 'bar',
|
||||
data: ecoData.value.biodiversity.value,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#E6A23C' },
|
||||
{ offset: 1, color: 'rgba(230, 162, 60, 0.3)' }
|
||||
])
|
||||
},
|
||||
barWidth: '15%'
|
||||
},
|
||||
{
|
||||
name: '植被覆盖率',
|
||||
type: 'line',
|
||||
data: ecoData.value.vegetation.value,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: '#67C23A'
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#67C23A',
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: item.color },
|
||||
{ offset: 1, color: 'rgba(0,0,0,0.1)' }
|
||||
])
|
||||
}
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
// 数据更新函数
|
||||
const updateData = () => {
|
||||
// 获取预警统计数据
|
||||
const fetchAlertData = async () => {
|
||||
try {
|
||||
const res = await getAlertStatisticsOverview()
|
||||
if (res.success && res.data) {
|
||||
// 更新各类预警的数量和状态
|
||||
Object.keys(alertData.value).forEach(key => {
|
||||
if (res.data[key]) {
|
||||
alertData.value[key].count = res.data[key].total || 0
|
||||
alertData.value[key].level = getAlertLevel(res.data[key].total || 0)
|
||||
alertData.value[key].color = getAlertColor(res.data[key].total || 0)
|
||||
|
||||
// 更新图表数据
|
||||
alertData.value[key].value = res.data[key].hourly_counts || Array(7).fill(0)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新图表
|
||||
updateChart()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预警统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警等级
|
||||
const getAlertLevel = (count) => {
|
||||
if (count === 0) return '正常'
|
||||
if (count < 3) return '轻微'
|
||||
if (count < 5) return '中等'
|
||||
return '严重'
|
||||
}
|
||||
|
||||
// 获取预警颜色
|
||||
const getAlertColor = (count) => {
|
||||
if (count === 0) return '#67C23A'
|
||||
if (count < 3) return '#E6A23C'
|
||||
if (count < 5) return '#F56C6C'
|
||||
return '#F56C6C'
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chart) return
|
||||
|
||||
// 更新时间
|
||||
const now = new Date()
|
||||
timeData.shift()
|
||||
timeData.push(now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0'))
|
||||
|
||||
// 更新数据
|
||||
Object.keys(ecoData.value).forEach(key => {
|
||||
const values = ecoData.value[key].value
|
||||
values.shift()
|
||||
values.push(Math.floor(70 + Math.random() * 30))
|
||||
})
|
||||
|
||||
chart.setOption({
|
||||
xAxis: {
|
||||
data: timeData
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: ecoData.value.airQuality.value
|
||||
},
|
||||
{
|
||||
data: ecoData.value.waterQuality.value
|
||||
},
|
||||
{
|
||||
data: ecoData.value.biodiversity.value
|
||||
},
|
||||
{
|
||||
data: ecoData.value.vegetation.value
|
||||
}
|
||||
]
|
||||
series: Object.values(alertData.value).map(item => ({
|
||||
name: item.name,
|
||||
data: item.value
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
let timer = null
|
||||
|
||||
onMounted(() => {
|
||||
initTimeData()
|
||||
initChart()
|
||||
// 自动更新数据
|
||||
timer = setInterval(updateData, 3000)
|
||||
fetchAlertData()
|
||||
// 每分钟更新一次数据
|
||||
timer = setInterval(fetchAlertData, 60000)
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', () => {
|
||||
@ -222,7 +221,7 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div class="top-card">
|
||||
<div class="card-header">
|
||||
<div class="title">生态指标</div>
|
||||
<div class="title">预警监测</div>
|
||||
<div class="update-time">实时监测中</div>
|
||||
</div>
|
||||
|
||||
@ -230,12 +229,12 @@ onUnmounted(() => {
|
||||
|
||||
<div class="indicators-list">
|
||||
<div
|
||||
v-for="(item, key) in ecoData"
|
||||
v-for="(item, key) in alertData"
|
||||
:key="key"
|
||||
class="indicator-item"
|
||||
>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="value" :style="{ color: item.color }">{{ item.value[item.value.length - 1] }}</span>
|
||||
<span class="value" :style="{ color: item.color }">{{ item.count }}</span>
|
||||
<span class="level" :style="{ background: item.color }">{{ item.level }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,202 @@
|
||||
<script setup>
|
||||
// 后续添加具体实现
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getAlertStatisticsOverview } from '@/api/monitor/alert'
|
||||
|
||||
let chart = null
|
||||
const chartRef = ref(null)
|
||||
|
||||
// 预警数据
|
||||
const alertData = ref({
|
||||
total: 0,
|
||||
water_quality: {
|
||||
name: '水质预警',
|
||||
total: 0,
|
||||
color: '#36CFFF'
|
||||
},
|
||||
air_quality: {
|
||||
name: '空气质量预警',
|
||||
total: 0,
|
||||
color: '#FFB72C'
|
||||
},
|
||||
ecosystem: {
|
||||
name: '生态系统预警',
|
||||
total: 0,
|
||||
color: '#4EF568'
|
||||
},
|
||||
weather: {
|
||||
name: '气象预警',
|
||||
total: 0,
|
||||
color: '#FF36D9'
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chart = echarts.init(chartRef.value)
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '15%',
|
||||
top: '10%',
|
||||
bottom: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: function(params) {
|
||||
const data = params[0]
|
||||
return `${data.name}<br/>预警次数:${data.value}<br/>占比:${((data.value / alertData.value.total) * 100).toFixed(1)}%`
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)',
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: '40%',
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
formatter: function(params) {
|
||||
return params.value + '次'
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: [0, 4, 4, 0]
|
||||
},
|
||||
data: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
// 获取预警统计数据
|
||||
const fetchAlertData = async () => {
|
||||
try {
|
||||
const res = await getAlertStatisticsOverview()
|
||||
if (res.success && res.data) {
|
||||
// 更新总数
|
||||
alertData.value.total = Object.values(res.data).reduce((sum, item) => sum + (item.total || 0), 0)
|
||||
|
||||
// 更新各类预警数据
|
||||
Object.keys(alertData.value).forEach(key => {
|
||||
if (key !== 'total' && res.data[key]) {
|
||||
alertData.value[key].total = res.data[key].total || 0
|
||||
}
|
||||
})
|
||||
|
||||
// 处理图表数据
|
||||
const sortedData = Object.entries(alertData.value)
|
||||
.filter(([key]) => key !== 'total')
|
||||
.sort((a, b) => b[1].total - a[1].total)
|
||||
|
||||
const chartData = sortedData.map(([_, item]) => ({
|
||||
name: item.name,
|
||||
value: item.total,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
|
||||
{ offset: 0, color: item.color.replace('FF', '40') },
|
||||
{ offset: 1, color: item.color }
|
||||
])
|
||||
}
|
||||
}))
|
||||
|
||||
chart.setOption({
|
||||
yAxis: {
|
||||
data: sortedData.map(([_, item]) => item.name)
|
||||
},
|
||||
series: [{
|
||||
data: chartData
|
||||
}]
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预警统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新数据
|
||||
let timer = null
|
||||
const startAutoRefresh = () => {
|
||||
fetchAlertData()
|
||||
timer = setInterval(fetchAlertData, 60000) // 每分钟更新一次
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chart?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
startAutoRefresh()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
chart = null
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bottom-card">
|
||||
<div class="card-title">告警信息</div>
|
||||
<div class="card-content">
|
||||
<!-- 临时占位内容 -->
|
||||
<div class="placeholder">告警信息内容区域</div>
|
||||
<div class="card-header">
|
||||
<div class="title">预警统计</div>
|
||||
<div class="total-alert">
|
||||
总预警数:<span class="value">{{ alertData.total }}</span>
|
||||
</div>
|
||||
<div class="update-time">实时监测中</div>
|
||||
</div>
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -16,23 +204,68 @@
|
||||
.bottom-card {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(6, 30, 93, 0.5);
|
||||
border-radius: 4px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
height: calc(100% - 32px);
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to bottom, #ffffff, #3fa7dd);
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.total-alert {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
|
||||
.value {
|
||||
color: #36CFFF;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: calc(100% - 60px);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.2; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.2; }
|
||||
}
|
||||
</style>
|
||||
@ -1,14 +1,170 @@
|
||||
<script setup>
|
||||
// 后续添加具体实现
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getAlertStatisticsByIndicator } from '@/api/monitor/alert'
|
||||
|
||||
let chart = null
|
||||
const chartRef = ref(null)
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chart = echarts.init(chartRef.value)
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params) {
|
||||
return `${params.name}<br/>预警次数:${params.value}<br/>占比:${params.percent}%`
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: '5%',
|
||||
top: 'middle',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
},
|
||||
formatter: (name) => {
|
||||
const data = option.series[0].data
|
||||
const item = data.find(v => v.name === name)
|
||||
return `${name} ${item ? item.value : 0}次`
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '指标预警',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['40%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: 'rgba(0, 0, 0, 0.2)',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: '{b}\n{d}%',
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
},
|
||||
labelLine: {
|
||||
length: 15,
|
||||
length2: 0,
|
||||
maxSurfaceAngle: 80,
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
},
|
||||
data: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
// 获取指标预警统计数据
|
||||
const fetchIndicatorData = async () => {
|
||||
try {
|
||||
const res = await getAlertStatisticsByIndicator()
|
||||
if (res.success && res.data) {
|
||||
// 将数据转换为饼图所需格式
|
||||
const chartData = Object.entries(res.data).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
itemStyle: {
|
||||
color: getRandomColor(name)
|
||||
}
|
||||
})).sort((a, b) => b.value - a.value) // 按数值从大到小排序
|
||||
|
||||
chart.setOption({
|
||||
series: [{
|
||||
data: chartData
|
||||
}]
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取指标预警统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成固定的渐变色
|
||||
const getRandomColor = (name) => {
|
||||
const colors = {
|
||||
'溶解氧': new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#36CFFF' },
|
||||
{ offset: 1, color: '#2861F5' }
|
||||
]),
|
||||
'pH值': new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#FFB72C' },
|
||||
{ offset: 1, color: '#F5612A' }
|
||||
]),
|
||||
'浊度': new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#4EF568' },
|
||||
{ offset: 1, color: '#2AB256' }
|
||||
]),
|
||||
'水温': new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#FF36D9' },
|
||||
{ offset: 1, color: '#C92AF5' }
|
||||
]),
|
||||
'氨氮': new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#36FFB0' },
|
||||
{ offset: 1, color: '#2AF5A1' }
|
||||
])
|
||||
}
|
||||
|
||||
return colors[name] || new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#7636FF' },
|
||||
{ offset: 1, color: '#2A3CF5' }
|
||||
])
|
||||
}
|
||||
|
||||
// 自动刷新数据
|
||||
let timer = null
|
||||
const startAutoRefresh = () => {
|
||||
fetchIndicatorData()
|
||||
timer = setInterval(fetchIndicatorData, 60000) // 每分钟更新一次
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chart?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
startAutoRefresh()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
chart = null
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="middle-card">
|
||||
<div class="card-title">重点指标</div>
|
||||
<div class="card-content">
|
||||
<!-- 临时占位内容 -->
|
||||
<div class="placeholder">重点指标内容区域</div>
|
||||
<div class="card-header">
|
||||
<div class="title">指标预警统计</div>
|
||||
<div class="update-time">实时监测中</div>
|
||||
</div>
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -16,23 +172,56 @@
|
||||
.middle-card {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(6, 30, 93, 0.5);
|
||||
border-radius: 4px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
height: calc(100% - 32px);
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: calc(100% - 60px);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.2; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.2; }
|
||||
}
|
||||
</style>
|
||||
@ -1,14 +1,163 @@
|
||||
<script setup>
|
||||
// 后续添加具体实现
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getAlertStatisticsByRule } from '@/api/monitor/alert'
|
||||
|
||||
let chart = null
|
||||
const chartRef = ref(null)
|
||||
|
||||
// 规则预警数据
|
||||
const ruleAlerts = ref([])
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chart = echarts.init(chartRef.value)
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
right: '5%',
|
||||
bottom: '10%',
|
||||
left: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)',
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '预警次数',
|
||||
type: 'bar',
|
||||
data: [],
|
||||
barWidth: '40%',
|
||||
itemStyle: {
|
||||
borderRadius: [0, 4, 4, 0]
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
color: '#fff',
|
||||
formatter: '{c}次'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
// 获取规则预警统计数据
|
||||
const fetchRuleAlertData = async () => {
|
||||
try {
|
||||
const res = await getAlertStatisticsByRule()
|
||||
if (res.success && res.data) {
|
||||
// 转换数据格式并排序
|
||||
const sortedData = Object.entries(res.data)
|
||||
.map(([name, value]) => ({ name, value }))
|
||||
.sort((a, b) => b.value - a.value)
|
||||
|
||||
// 更新图表数据
|
||||
chart.setOption({
|
||||
yAxis: {
|
||||
data: sortedData.map(item => item.name)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: sortedData.map(item => ({
|
||||
value: item.value,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: 'rgba(54, 207, 255, 0.2)' },
|
||||
{ offset: 1, color: '#36CFFF' }
|
||||
])
|
||||
}
|
||||
}))
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取规则预警统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新数据
|
||||
let timer = null
|
||||
const startAutoRefresh = () => {
|
||||
fetchRuleAlertData()
|
||||
timer = setInterval(fetchRuleAlertData, 60000) // 每分钟更新一次
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chart?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
startAutoRefresh()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
chart = null
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="top-card">
|
||||
<div class="card-title">天气信息</div>
|
||||
<div class="card-content">
|
||||
<!-- 临时占位内容 -->
|
||||
<div class="placeholder">天气信息内容区域</div>
|
||||
<div class="card-header">
|
||||
<div class="title">规则预警统计</div>
|
||||
<div class="update-time">实时监测中</div>
|
||||
</div>
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -16,23 +165,56 @@
|
||||
.top-card {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(6, 30, 93, 0.5);
|
||||
border-radius: 4px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
height: calc(100% - 32px);
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: calc(100% - 60px);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.2; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.2; }
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user