完善首页的页面

This commit is contained in:
wzclm 2025-03-04 19:55:53 +08:00
parent 4a5e37b5d9
commit d62b8267a8
3 changed files with 362 additions and 289 deletions

View File

@ -0,0 +1,25 @@
import request from '@/utils/request'
/**
* 获取物种统计信息
* @returns {Promise} 返回物种统计数据
*/
export function getSpeciesStatistics() {
return request.get('/api/admin/species/statistics/overview')
}
/**
* 获取巡护任务统计信息
* @returns {Promise} 返回巡护任务统计数据
*/
export function getPatrolStatistics() {
return request.get('/api/admin/patrol/records/statistics/overview')
}
/**
* 获取设备列表信息
* @returns {Promise} 返回设备列表数据
*/
export function getDeviceList() {
return request.get('/api/device/list')
}

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from "vue";
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
import { markRaw } from 'vue'
import {
@ -7,9 +7,9 @@ import {
DataAnalysis,
Location,
Document,
Timer,
Bell
Warning
} from "@element-plus/icons-vue";
import { getSpeciesStatistics, getPatrolStatistics, getDeviceList } from '@/api/dashboard'
// 使 markRaw
const icons = {
@ -17,41 +17,275 @@ const icons = {
DataAnalysis: markRaw(DataAnalysis),
Location: markRaw(Location),
Document: markRaw(Document),
Timer: markRaw(Timer),
Bell: markRaw(Bell)
Warning: markRaw(Warning)
};
//
const statistics = ref({
species: {
total: 128,
today: 12,
trend: "+8%",
type: "success",
icon: "Histogram",
//
const categoryOptions = [
{ label: '鸟类', value: 'bird' },
{ label: '哺乳类', value: 'mammal' },
{ label: '鱼类', value: 'fish' },
{ label: '两栖类', value: 'amphibian' },
{ label: '爬行类', value: 'reptile' },
{ label: '昆虫类', value: 'insect' },
{ label: '植物', value: 'plant' }
]
//
const categoryChartRef = ref(null)
let categoryChart = null
//
const statsCards = ref([
{
title: '物种监测',
icon: icons.Monitor,
value: '0',
unit: '种',
change: { value: '0', label: '今日新增' },
color: '#1890FF',
bgColor: 'linear-gradient(120deg, #0072FF 0%, #00C6FF 100%)',
features: ['实时监测', '智能识别', '行为分析', '分布追踪']
},
environment: {
normal: 22,
abnormal: 2,
trend: "normal",
type: "warning",
icon: "Monitor",
{
title: '环境监测',
icon: icons.DataAnalysis,
value: '2',
unit: '点',
change: { value: '2', label: '异常' },
color: '#F5222D',
bgColor: 'linear-gradient(120deg, #FF416C 0%, #FF4B2B 100%)',
features: ['水质监测', '空气监测', '土壤监测', '气象监测']
},
patrol: {
total: 12,
completed: 8,
progress: "66%",
type: "primary",
icon: "Location",
{
title: '巡护任务',
icon: icons.Location,
value: '0',
unit: '个',
change: { value: '0%', label: '完成率' },
color: '#52C41A',
bgColor: 'linear-gradient(120deg, #00B09B 0%, #96C93D 100%)',
features: ['智能派单', '轨迹记录', '实时通讯', '数据采集']
},
devices: {
total: 36,
online: 32,
rate: "88.9%",
type: "info",
icon: "Connection",
},
});
{
title: '设备状态',
icon: icons.Monitor,
value: '0',
unit: '台',
change: { value: '0%', label: '在线率' },
color: '#722ED1',
bgColor: 'linear-gradient(120deg, #7F00FF 0%, #E100FF 100%)',
features: ['状态监控', '故障预警', '维护管理', '性能分析']
}
]);
//
const deviceData = ref({
total: 0,
online: 0
})
//
const initCategoryChart = () => {
if (!categoryChartRef.value) return
categoryChart = echarts.init(categoryChartRef.value)
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}种 ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle',
textStyle: {
color: '#303133'
}
},
series: [
{
name: '物种数量',
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {c}种'
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
data: []
}
]
}
categoryChart.setOption(option)
}
//
const updateCategoryChart = (data) => {
if (!categoryChart) return
//
const categoryData = Object.entries(data.categories)
.filter(([_, count]) => count.total_count > 0)
.map(([category, count]) => ({
name: categoryOptions.find(item => item.value === category)?.label || category,
value: parseInt(count.total_count)
}))
.sort((a, b) => b.value - a.value)
categoryChart.setOption({
series: [{
data: categoryData
}]
})
}
//
const fetchSpeciesData = async () => {
try {
const res = await getSpeciesStatistics()
if (res.success && res.data) {
//
const totalSpecies = Object.values(res.data.categories).reduce((sum, category) =>
sum + (parseInt(category.total_count) || 0), 0)
const todayNew = Object.values(res.data.categories).reduce((sum, category) =>
sum + (parseInt(category.today_count) || 0), 0)
//
statsCards.value[0].value = String(totalSpecies || 0)
statsCards.value[0].change.value = `+${todayNew || 0}`
//
updateCategoryChart(res.data)
}
} catch (error) {
console.error('获取物种统计数据失败:', error)
}
}
//
const fetchPatrolData = async () => {
try {
const res = await getPatrolStatistics()
if (res.success && res.data) {
const { overview } = res.data
const progress = ((overview.completed_count / overview.total_count) * 100).toFixed(1)
//
statsCards.value[2].value = String(overview.total_count)
statsCards.value[2].change.value = `${progress}%`
}
} catch (error) {
console.error('获取巡护任务统计数据失败:', error)
}
}
//
const fetchDeviceData = async () => {
try {
const res = await getDeviceList()
if (res.success && res.data?.list) {
const deviceList = res.data.list
deviceData.value = {
total: deviceList.length,
online: deviceList.filter(device => device.status?.code === 1).length
}
//
statsCards.value[3].value = String(deviceData.value.total)
statsCards.value[3].change.value = `${((deviceData.value.online / deviceData.value.total) * 100).toFixed(1)}%`
//
statsCards.value[3].features = [
`在线: ${deviceData.value.online}`,
`离线: ${deviceData.value.total - deviceData.value.online}`,
'故障预警',
'性能分析'
]
}
} catch (error) {
console.error('获取设备列表数据失败:', error)
}
}
//
const getCategoryName = (key) => {
const categoryNames = {
bird: '鸟类',
mammal: '哺乳类',
fish: '鱼类',
amphibian: '两栖类',
reptile: '爬行类',
insect: '昆虫类',
plant: '植物'
}
return categoryNames[key] || key
}
//
const getCategoryColor = (key) => {
const categoryColors = {
bird: '#409EFF',
mammal: '#67C23A',
fish: '#E6A23C',
amphibian: '#F56C6C',
reptile: '#909399',
insect: '#9B59B6',
plant: '#2ECC71'
}
return categoryColors[key] || '#409EFF'
}
//
const updateDistributionChart = (data) => {
const chartDom = document.getElementById("distributionChart");
if (!chartDom) return;
const myChart = echarts.init(chartDom);
myChart.setOption({
series: [{
data: data
}]
})
}
//
const initData = async () => {
try {
await Promise.all([
fetchSpeciesData(),
fetchPatrolData(),
fetchDeviceData()
])
} catch (error) {
console.error('初始化数据失败:', error)
}
}
//
let timer = null
const startAutoRefresh = () => {
fetchStatisticsData()
timer = setInterval(fetchStatisticsData, 60000) //
}
//
const initTrendChart = () => {
@ -300,78 +534,52 @@ const initDistributionChart = () => {
});
};
//
const activities = ref([
{
title: "系统更新",
desc: "系统版本更新到 v2.0",
time: "刚刚",
type: "primary",
icon: icons.Timer
},
{
icon: icons.Bell,
type: "warning",
title: "环境预警",
desc: "B区水质监测点位出现异常数据",
time: "30分钟前",
},
{
icon: icons.Document,
type: "success",
title: "日报生成",
desc: "系统自动生成了昨日监测报告",
time: "1小时前",
},
]);
//
const fetchStatisticsData = async () => {
try {
const res = await getSpeciesStatistics()
if (res.success && res.data) {
//
const totalSpecies = Object.values(res.data.categories).reduce(
(sum, item) => sum + (parseInt(item.total_count) || 0), 0
)
const newSpecies = Object.values(res.data.categories).reduce(
(sum, item) => sum + (parseInt(item.today_count) || 0), 0
)
statsCards.value[0].value = String(totalSpecies || 0)
statsCards.value[0].change.value = `+${newSpecies || 0}`
const statsCards = ref([
{
title: '物种监测',
icon: icons.Monitor,
value: '128',
unit: '种',
change: { value: '+12', label: '今日新增' },
color: '#1890FF',
bgColor: 'linear-gradient(120deg, #0072FF 0%, #00C6FF 100%)',
features: ['实时监测', '智能识别', '行为分析', '分布追踪']
},
{
title: '环境监测',
icon: icons.DataAnalysis,
value: '22',
unit: '点',
change: { value: '2', label: '异常' },
color: '#F5222D',
bgColor: 'linear-gradient(120deg, #FF416C 0%, #FF4B2B 100%)',
features: ['水质监测', '空气监测', '土壤监测', '气象监测']
},
{
title: '巡护任务',
icon: icons.Location,
value: '8',
unit: '个',
change: { value: '66%', label: '完成率' },
color: '#52C41A',
bgColor: 'linear-gradient(120deg, #00B09B 0%, #96C93D 100%)',
features: ['智能派单', '轨迹记录', '实时通讯', '数据采集']
},
{
title: '设备状态',
icon: icons.Connection,
value: '32',
unit: '台',
change: { value: '88.9%', label: '在线率' },
color: '#722ED1',
bgColor: 'linear-gradient(120deg, #7F00FF 0%, #E100FF 100%)',
features: ['状态监控', '故障预警', '维护管理', '性能分析']
//
updateCategoryChart(res.data)
}
} catch (error) {
console.error('获取统计数据失败:', error)
}
]);
}
onMounted(() => {
initData()
startAutoRefresh()
initTrendChart();
initDistributionChart();
initCategoryChart()
window.addEventListener('resize', () => {
categoryChart?.resize()
})
});
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
if (categoryChart) {
categoryChart.dispose()
categoryChart = null
}
window.removeEventListener('resize', () => {
categoryChart?.resize()
})
})
</script>
<template>
@ -412,50 +620,16 @@ onMounted(() => {
</div>
<!-- 图表区域 -->
<el-row :gutter="20" class="mb-20">
<el-col :span="16">
<el-card class="chart-card" shadow="hover">
<div id="trendChart" style="height: 400px"></div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="chart-card" shadow="hover">
<div id="distributionChart" style="height: 400px"></div>
</el-card>
</el-col>
</el-row>
<!-- 动态信息区域 -->
<el-row>
<el-col :span="24">
<el-card class="activity-card" shadow="hover">
<template #header>
<div class="card-header">
<span>动态信息</span>
</div>
</template>
<div class="activity-list">
<div
v-for="(item, index) in activities"
:key="index"
class="activity-item"
:class="{ 'with-border': index !== activities.length - 1 }"
>
<div class="activity-icon" :class="item.type">
<el-icon>
<component :is="item.icon" />
</el-icon>
</div>
<div class="activity-content">
<div class="activity-title">{{ item.title }}</div>
<div class="activity-desc">{{ item.desc }}</div>
</div>
<div class="activity-time">{{ item.time }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<div class="charts-container">
<div class="chart-item">
<div class="chart-title">物种类别统计</div>
<div ref="categoryChartRef" class="chart-content"></div>
</div>
<div class="chart-item">
<div class="chart-title">趋势统计</div>
<div id="trendChart" class="chart-content"></div>
</div>
</div>
</div>
</template>
@ -616,76 +790,23 @@ onMounted(() => {
}
}
.activity-card {
border: none;
border-radius: 8px;
transition: all 0.3s;
.charts-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
}
}
.chart-item {
flex: 1;
.activity-list {
.activity-item {
display: flex;
align-items: flex-start;
padding: 16px 0;
&.with-border {
border-bottom: 1px solid #f0f2f5;
.chart-title {
font-size: 16px;
font-weight: 500;
color: v.$text-primary;
margin-bottom: 8px;
}
.activity-icon {
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
&.primary {
background: rgba(64, 158, 255, 0.1);
color: v.$primary-color;
}
&.warning {
background: rgba(230, 162, 60, 0.1);
color: v.$warning-color;
}
&.success {
background: rgba(103, 194, 58, 0.1);
color: v.$success-color;
}
.el-icon {
font-size: 16px;
}
}
.activity-content {
flex: 1;
.activity-title {
font-size: 14px;
font-weight: 500;
color: v.$text-primary;
margin-bottom: 4px;
}
.activity-desc {
font-size: 13px;
color: v.$text-secondary;
}
}
.activity-time {
font-size: 12px;
color: v.$text-secondary;
margin-left: 16px;
.chart-content {
height: 400px;
}
}
}

View File

@ -100,9 +100,7 @@ const statistics = ref({
})
//
const categoryChartRef = ref(null)
const protectionChartRef = ref(null)
let categoryChart = null
let protectionChart = null
// URL
@ -126,9 +124,6 @@ const getFullImageUrl = (url) => {
//
const initCharts = () => {
if (categoryChartRef.value) {
categoryChart = echarts.init(categoryChartRef.value)
}
if (protectionChartRef.value) {
protectionChart = echarts.init(protectionChartRef.value)
}
@ -136,75 +131,14 @@ const initCharts = () => {
//
const updateCharts = () => {
//
const categoryData = reverseArray(
Object.entries(statistics.value.categories)
.filter(([_, count]) => count.total_count > 0)
.map(([category, count]) => ({
name: categoryOptions.find(item => item.value === category)?.label || category,
value: count.total_count
}))
.sort((a, b) => a.value - b.value)
)
//
const protectionData = reverseArray(
Object.entries(statistics.value.protection_levels)
.filter(([_, count]) => count > 0)
.map(([level, count]) => ({
name: protectionLevelOptions.find(item => item.value === level)?.label || level,
value: count
}))
.sort((a, b) => a.value - b.value)
)
//
categoryChart?.setOption({
title: {
text: '物种类别统计',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c}种'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle'
},
series: [
{
name: '物种数量',
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {c}种'
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
data: categoryData
}
]
})
const protectionData = Object.entries(statistics.value.protection_levels)
.filter(([_, count]) => count > 0)
.map(([level, count]) => ({
name: protectionLevelOptions.find(item => item.value === level)?.label || level,
value: count
}))
.sort((a, b) => b.value - a.value)
//
protectionChart?.setOption({
@ -265,7 +199,6 @@ const updateCharts = () => {
//
const handleResize = () => {
categoryChart?.resize()
protectionChart?.resize()
}
@ -520,7 +453,6 @@ onMounted(() => {
onUnmounted(() => {
//
categoryChart?.dispose()
protectionChart?.dispose()
window.removeEventListener('resize', handleResize)
})
@ -530,12 +462,7 @@ onUnmounted(() => {
<div class="app-container">
<!-- 统计信息展示 -->
<el-row :gutter="20" class="statistics-container">
<el-col :span="12">
<el-card>
<div ref="categoryChartRef" style="height: 400px"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-col :span="24">
<el-card>
<div ref="protectionChartRef" style="height: 400px"></div>
</el-card>