完善可视化大屏的图表

This commit is contained in:
wzclm 2025-03-04 16:15:52 +08:00
parent f401f40a6e
commit 1265cf7553
9 changed files with 1714 additions and 211 deletions

33
src/api/monitor/alert.js Normal file
View 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
View 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 })
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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