<template>
<div class="curved-dashboard">
<!-- 顶部标题 -->
<div class="title"></div>
<!-- 3D弧形面板容器(6个模块,对称布局) -->
<div class="dashboard-container">
<!-- 模块1:左侧数据概览 -->
<div class="dashboard-module module-1" @click="openDialog(1)">
<div class="module-header">监视预警</div>
<div class="module-content">
<div class="stats-grid">
<waterStorage />
</div>
<div class="stats-grid">
<safetyMonitoring />
</div>
<div class="stats-grid">
<warningStatistics />
</div>
</div>
</div>
<!-- 模块2:左中水量调度模块 -->
<div class="dashboard-module module-2" @click="openDialog(2)">
<div class="module-header">水量调度模块</div>
<div class="module-content">
<div class="stats-grid">
<waterFlowChart />
</div>
<div class="stats-grid">
<waterSupplyChart />
</div>
<div class="stats-grid">
<waterDemandChart />
</div>
</div>
</div>
<!-- 模块3:中间3D核心 -->
<div class="dashboard-module module-3" @click="openDialog(3)">
<div class="module-header">安全监管模块</div>
<div class="module-content core">
<div class="stats-grid">
<rainMonitorChart />
</div>
<div class="stats-grid">
<waterMonitorChart />
</div>
<div>
<floodProtectionChart />
</div>
</div>
</div>
<!-- 模块4:右中资产分布 -->
<div class="dashboard-module module-4" @click="openDialog(4)">
<div class="module-header">水质保障模块</div>
<div class="module-content">
<div class="stats-grid">
<waterQualityDistribution />
</div>
<div class="stats-grid">
<monitoringDataChart />
</div>
<div class="stats-grid">
<siteComparisonRadar />
</div>
</div>
</div>
<!-- 模块5:右侧数据质量监控 -->
<div class="dashboard-module module-5" @click="openDialog(5)">
<div class="module-header">运行维护</div>
<div class="module-content">
<div class="stats-grid">
<dronePatrol />
</div>
<div class="stats-grid">
<engineering />
</div>
<div class="stats-grid">
<equipmentHealth />
</div>
</div>
</div>
<!-- 新增模块6:最右侧系统性能监控 -->
<div class="dashboard-module module-6" @click="openDialog(6)">
<div class="module-header">应急处置模块</div>
<div class="module-content">
<div class="stats-grid">
<eventTypeWordCloud />
</div>
<div class="stats-grid">
<drillPlanCalendar />
</div>
<div class="stats-grid">
<emergencySupport />
</div>
</div>
</div>
</div>
<!-- 弹窗遮罩层 -->
<div v-if="showDialog" class="dialog-overlay" @click="closeDialog">
<!-- 弹窗内容容器 -->
<div class="dialog-container" @click.stop>
<div class="dialog-header">
<h2>{{ dialogTitle }}</h2>
<button class="close-btn" @click="closeDialog">×</button>
</div>
<div class="dialog-content">
<!-- 根据当前模块显示对应内容 -->
<div v-if="currentModule === 1" class="module-1-content">
<waterStorageMagnify :isDialog="true" />
<safetyMonitoringMagnify :isDialog="true" />
<warningStatisticsMagnify :isDialog="true" />
</div>
<div v-if="currentModule === 2" class="module-2-content">
<waterFlowChartMagnify :isDialog="true" />
<waterSupplyChartMagnify :isDialog="true" />
<waterDemandChartMagnify :isDialog="true" />
</div>
<div v-if="currentModule === 3" class="module-3-content">
<rainMonitorChartMagnify :isDialog="true" />
<waterMonitorChartMagnify :isDialog="true" />
<floodProtectionChartMagnify :isDialog="true" />
</div>
<div v-if="currentModule === 4" class="module-4-content">
<waterQualityDistributionMagnify :isDialog="true" />
<monitoringDataChartMagnify :isDialog="true" />
<siteComparisonRadarMagnify :isDialog="true" />
</div>
<div v-if="currentModule === 5" class="module-5-content">
<dronePatrolMagnify :isDialog="true" />
<engineeringMagnify :isDialog="true" />
<equipmentHealthMagnify :isDialog="true" />
</div>
<div v-if="currentModule === 6" class="module-6-content">
<eventTypeWordCloudMagnify :isDialog="true" />
<drillPlanCalendarMagnify :isDialog="true" />
<emergencySupportMagnify :isDialog="true" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
computed
} from 'vue';
import waterStorage from "./components/reduce/surveillanceWarning/waterStorage.vue"
import warningStatistics from "./components/reduce/surveillanceWarning/warningStatistics.vue"
import safetyMonitoring from "./components/reduce/surveillanceWarning/safetyMonitoring.vue"
import waterFlowChart from "./components/reduce/waterDispatching/waterFlowChart.vue"
import waterSupplyChart from "./components/reduce/waterDispatching/waterSupplyChart.vue"
import waterDemandChart from "./components/reduce/waterDispatching/waterDemandChart.vue"
import rainMonitorChart from "./components/reduce/safetySupervision/rainMonitorChart.vue"
import waterMonitorChart from "./components/reduce/safetySupervision/waterMonitorChart.vue"
import floodProtectionChart from "./components/reduce/safetySupervision/floodProtectionChart.vue"
import waterQualityDistribution from "./components/reduce/waterQualityAssurance/waterQualityDistribution.vue"
import monitoringDataChart from "./components/reduce/waterQualityAssurance/monitoringDataChart.vue"
import siteComparisonRadar from "./components/reduce/waterQualityAssurance/siteComparisonRadar.vue"
import dronePatrol from "./components/reduce/maintenance/dronePatrol.vue"
import engineering from "./components/reduce/maintenance/engineering.vue"
import equipmentHealth from "./components/reduce/maintenance/equipmentHealth.vue"
import eventTypeWordCloud from "./components/reduce/emergencyResponse/eventTypeWordCloud.vue"
import drillPlanCalendar from "./components/reduce/emergencyResponse/drillPlanCalendar.vue"
import emergencySupport from "./components/reduce/emergencyResponse/emergencySupport.vue"
import waterStorageMagnify from "./components/magnify/surveillanceWarning/waterStorage.vue"
import safetyMonitoringMagnify from "./components/magnify/surveillanceWarning/safetyMonitoring.vue"
import warningStatisticsMagnify from "./components/magnify/surveillanceWarning/warningStatistics.vue"
import waterFlowChartMagnify from "./components/magnify/waterDispatching/waterFlowChart.vue"
import waterSupplyChartMagnify from "./components/magnify/waterDispatching/waterSupplyChart.vue"
import waterDemandChartMagnify from "./components/magnify/waterDispatching/waterDemandChart.vue"
import rainMonitorChartMagnify from "./components/magnify/safetySupervision/rainMonitorChart.vue"
import waterMonitorChartMagnify from "./components/magnify/safetySupervision/waterMonitorChart.vue"
import floodProtectionChartMagnify from "./components/magnify/safetySupervision/floodProtectionChart.vue"
import waterQualityDistributionMagnify from "./components/magnify/waterQualityAssurance/waterQualityDistribution.vue"
import monitoringDataChartMagnify from "./components/magnify/waterQualityAssurance/monitoringDataChart.vue"
import siteComparisonRadarMagnify from "./components/magnify/waterQualityAssurance/siteComparisonRadar.vue"
import dronePatrolMagnify from "./components/magnify/maintenance/dronePatrol.vue"
import engineeringMagnify from "./components/magnify/maintenance/engineering.vue"
import equipmentHealthMagnify from "./components/magnify/maintenance/equipmentHealth.vue"
import eventTypeWordCloudMagnify from "./components/magnify/emergencyResponse/eventTypeWordCloud.vue"
import drillPlanCalendarMagnify from "./components/magnify/emergencyResponse/drillPlanCalendar.vue"
import emergencySupportMagnify from "./components/magnify/emergencyResponse/emergencySupport.vue"
// 响应式数据
const showDialog = ref(false);
const currentModule = ref(1);
// 计算属性:根据当前模块返回弹窗标题
const dialogTitle = computed(() => {
const titles = {
1: "监视预警",
2: "水量调度模块",
3: "安全监管模块",
4: "水质保障模块",
5: "运行维护",
6: "应急处置模块"
};
return titles[currentModule.value] || "模块详情";
});
// 方法:打开弹窗
const openDialog = (moduleId) => {
currentModule.value = moduleId;
showDialog.value = true;
};
// 方法:关闭弹窗
const closeDialog = () => {
showDialog.value = false;
};
</script>
<style scoped>
/* 全局背景:深青蓝渐变,匹配原图色调 */
.curved-dashboard {
width: 100vw;
height: 100vh;
background: url("@/assets/imgs/底图.gif");
background-size: 100% 100%;
position: relative;
overflow: hidden;
}
/* 顶部标题:发光边框+科技感样式 */
.title {
position: absolute;
top: 40px;
left: 20%;
width: 60%;
height: 10%;
background: url("@/assets/imgs/标题@1x.png");
background-size: 100% 100%;
padding: 8px 25px;
}
/* 3D容器:perspective控制深度,确保弧形自然 */
.dashboard-container {
width: 80%;
height: 680px;
position: relative;
perspective: 2200px;
display: flex;
justify-content: center;
align-items: center;
margin-top: 100px;
margin-left: 10%;
}
/* 单个模块:半透明蓝+发光边框,匹配原图质感 */
.dashboard-module {
position: absolute;
width: 260px;
height: 420px;
background: rgba(0, 31, 88, 0.7);
border: 1px solid #35ABFF;
border-radius: 8px;
padding: 18px;
box-sizing: border-box;
color: #e0e0e0;
transform-style: preserve-3d;
backface-visibility: hidden;
box-shadow: 0px 4px 10px 0px rgba(65, 176, 255, 0.5), 0px -4px 10px 0px rgba(65, 176, 255, 0.5);
transition: all 0.3s ease;
cursor: pointer;
}
/* 模块hover:增强发光,突出交互 */
.dashboard-module:hover {
box-shadow: 0 0 20px rgba(64, 189, 245, 0.6);
border-color: #40bdf5;
background: rgba(0, 31, 88, 0.8);
z-index: 10;
}
/* 6个模块的3D变换参数(对称凹形布局) */
.module-1 {
transform: rotateY(50deg) translateX(-855px) translateZ(50px);
--rotate-y: 5deg;
}
.module-2 {
transform: rotateY(45deg) translateX(-570px) translateZ(90px);
--rotate-y: -12deg;
}
.module-3 {
transform: rotateY(20deg) translateX(-220px) translateZ(240px);
--rotate-y: 0deg;
}
.module-4 {
transform: rotateY(-20deg) translateX(220px) translateZ(240px);
--rotate-y: 12deg;
}
.module-5 {
transform: rotateY(-45deg) translateX(570px) translateZ(90px);
--rotate-y: 25deg;
}
/* 新增模块6:最右侧,与左侧模块1对称 */
.module-6 {
transform: rotateY(-50deg) translateX(855px) translateZ(50px);
--rotate-y: 30deg;
}
.module-header {
font-size: 16px;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(64, 189, 245, 0.2);
color: #40bdf5;
}
.module-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 30px);
}
/* 图表占位符:形状、颜色1:1还原原图 */
.chart-placeholder {
width: 100%;
height: 120px;
border-radius: 6px;
background: rgba(64, 189, 245, 0.1);
position: relative;
overflow: hidden;
border: 1px solid rgba(64, 189, 245, 0.2);
}
/* 统计卡片:网格布局+发光边框 */
.stats-grid,
.radar-stats {
margin-bottom: 8px;
}
/* 弹窗遮罩层样式 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
animation: fadeIn 0.3s ease;
}
/* 弹窗容器样式 */
.dialog-container {
width: 85%;
height: 85%;
background: rgba(0, 31, 88, 0.95);
border: 2px solid #35ABFF;
border-radius: 12px;
box-shadow: 0 0 40px rgba(53, 171, 255, 0.5);
display: flex;
flex-direction: column;
overflow: hidden;
animation: scaleIn 0.3s ease;
}
/* 弹窗头部 */
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 25px;
background: rgba(0, 20, 60, 0.8);
border-bottom: 1px solid rgba(53, 171, 255, 0.3);
}
.dialog-header h2 {
color: #40bdf5;
margin: 0;
font-size: 22px;
font-weight: 600;
}
.close-btn {
background: transparent;
border: none;
color: #40bdf5;
font-size: 28px;
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.close-btn:hover {
background: rgba(64, 189, 245, 0.2);
transform: scale(1.1);
}
/* 弹窗内容区域 */
.dialog-content {
flex: 1;
padding: 25px;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
align-content: flex-start;
}
/* 弹窗中的图表容器样式 */
.dialog-content :deep(.stats-grid) {
background: rgba(64, 189, 245, 0.1);
border: 1px solid rgba(64, 189, 245, 0.3);
border-radius: 8px;
padding: 15px;
height: 200px;
display: flex;
/* flex-direction: column; */
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 响应式调整 */
@media (max-width: 1200px) {
.dialog-content {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.dialog-container {
width: 95%;
height: 90%;
}
.dialog-content {
grid-template-columns: 1fr;
}
}
/* 中间3D地球:核心视觉+呼吸动画+连接线条 */
.core {
align-items: center;
justify-content: center;
}
.globe {
position: relative;
width: 180px;
height: 180px;
margin: 0 auto;
}
.globe-sphere {
width: 100%;
height: 100%;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #40bdf5, #08152d);
box-shadow: 0 0 30px rgba(64, 189, 245, 0.6);
animation: pulse 3s infinite ease-in-out;
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 30px rgba(64, 189, 245, 0.6);
}
50% {
box-shadow: 0 0 40px rgba(64, 189, 245, 0.8);
}
}
.globe-lines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-radial-gradient(circle, transparent 0%, rgba(64, 189, 245, 0.2) 5%, transparent 10%);
border-radius: 50%;
}
.globe-connections {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
linear-gradient(45deg, transparent 49%, rgba(64, 189, 245, 0.3) 50%, transparent 51%),
linear-gradient(135deg, transparent 49%, rgba(64, 189, 245, 0.3) 50%, transparent 51%);
border-radius: 50%;
}
/* 底部光效与网格:多层环形+科技纹理,匹配原图地面 */
/* .floor-effect {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 420px;
background: transparent;
} */
.floor-rings {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 850px;
height: 320px;
background: radial-gradient(circle, rgba(64, 189, 245, 0.18) 0%, transparent 70%);
border-radius: 50%;
}
.floor-rings::before,
.floor-rings::after,
.floor-rings::before::before {
content: '';
position: absolute;
border: 2px solid rgba(64, 189, 245, 0.35);
border-radius: 50%;
}
.floor-rings::before {
top: 70px;
left: 70px;
right: 70px;
bottom: 70px;
}
.floor-rings::after {
top: 140px;
left: 140px;
right: 140px;
bottom: 140px;
}
.module-1-content{
display: flex;
justify-content: space-between;
padding-top: 5vh;
align-items: center;
}
.module-2-content{
display: flex;
justify-content: space-between;
padding-top: 5vh;
align-items: center;
}
.module-3-content{
display: flex;
justify-content: space-between;
padding-top: 5vh;
align-items: center;
}
.module-4-content{
display: flex;
justify-content: space-between;
padding-top: 5vh;
align-items: center;
}
.module-5-content{
display: flex;
justify-content: space-between;
padding-top: 5vh;
align-items: center;
}
.module-6-content{
display: flex;
justify-content: space-between;
padding-top: 5vh;
align-items: center;
}
</style>
//drillPlanCalendar
<template>
<div class="custom-table-container">
<table class="custom-table">
<!-- 表头 -->
<thead>
<tr>
<th>演练次数</th>
<th>参与人数</th>
<th>完成率</th>
</tr>
</thead>
<!-- 表体 -->
<tbody>
<tr v-for="item in drillPlans" :key="item.id">
<td>{{ item.typeNum }}(次/年)</td>
<td>{{ item.participants }}(人)</td>
<td>{{ item.completionRate }}%</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
// 表格数据
const drillPlans = ref([
{ id: 1, typeNum: '2', participants: 120, completionRate: 95 },
{ id: 2, typeNum: '5', participants: 45, completionRate: 100 },
{ id: 3, typeNum: '7', participants: 200, completionRate: 60 },
{ id: 4, typeNum: '8', participants: 85, completionRate: 0 },
{ id: 5, typeNum: '2', participants: 30, completionRate: 0 },
]);
</script>
<style lang="css" scoped>
/* 容器样式 */
.custom-table-container {
width: 12.7vw; /* 缩小后的宽度 */
height: 13vh; /* 缩小后的高度 */
margin-left: -0.5vw;
border-radius: 8px;
border: 1px solid #004a99;
overflow: hidden; /* 隐藏外部滚动 */
}
/* 表格整体样式 */
.custom-table {
width: 100%;
height: 100%;
border-collapse: collapse;
font-size: calc(0.5vw); /* 缩小后的字体大小 */
color: #ffffff;
background-color: transparent;
display: block; /* 改为块级显示 */
}
/* 表头固定 */
.custom-table thead {
display: block;
width: 100%;
background: linear-gradient(180deg, rgba(22, 117, 196, 0) 0%, rgba(22, 117, 196, 0.7) 100%);
}
/* 表头行 */
.custom-table thead tr {
display: table;
width: 100%;
table-layout: fixed;
}
/* 表头单元格 */
.custom-table th {
text-align: center;
padding: 2px 4px; /* 缩小内边距 */
border-right: 1px solid #004a99;
border-bottom: 1px solid #004a99;
line-height: 1.2; /* 缩小行高 */
}
/* 表体滚动 */
.custom-table tbody {
display: block;
height: calc(100% - 10px); /* 减去表头高度 */
overflow-y: auto;
width: 100%;
}
/* 表体行 */
.custom-table tbody tr {
display: table;
width: 100%;
table-layout: fixed;
}
/* 表体单元格 */
.custom-table td {
padding: 2px 4px; /* 缩小内边距 */
text-align: center;
border-right: 1px solid #004a99;
border-bottom: 1px solid #004a99;
line-height: 1.2; /* 缩小行高 */
white-space: nowrap; /* 防止文字换行 */
overflow: hidden; /* 隐藏超出内容 */
text-overflow: ellipsis; /* 超出部分显示省略号 */
}
/* 列宽设置 */
.custom-table th:nth-child(1),
.custom-table td:nth-child(1) {
width: 40%;
}
.custom-table th:nth-child(2),
.custom-table td:nth-child(2) {
width: 30%;
}
.custom-table th:nth-child(3),
.custom-table td:nth-child(3) {
width: 30%;
}
/* 移除最后一列的右边框 */
.custom-table th:last-child,
.custom-table td:last-child {
border-right: none;
}
/* 行 hover 效果 */
.custom-table tbody tr:hover td {
background-color: rgba(0, 74, 153, 0.3);
}
/* 自定义滚动条样式 */
.custom-table tbody::-webkit-scrollbar {
width: 6px;
}
.custom-table tbody::-webkit-scrollbar-track {
background: rgba(0, 74, 153, 0.2);
border-radius: 3px;
}
.custom-table tbody::-webkit-scrollbar-thumb {
background: rgba(0, 74, 153, 0.6);
border-radius: 3px;
}
.custom-table tbody::-webkit-scrollbar-thumb:hover {
background: rgba(0, 74, 153, 0.8);
}
</style>
// emergencySupport
<template>
<div class="dashboard">
<!-- 物资库存仪表盘 -->
<div class="gauge-item">
<div class="gauge-title">物资库存 (万件)</div>
<div ref="materialGauge" class="gauge-chart"></div>
</div>
<!-- 队伍人数仪表盘 -->
<div class="gauge-item">
<div class="gauge-title">队伍人数 (人)</div>
<div ref="teamGauge" class="gauge-chart"></div>
</div>
<!-- 预案完备率仪表盘 -->
<div class="gauge-item">
<div class="gauge-title">预案完备率 (%)</div>
<div ref="planGauge" class="gauge-chart"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
// 定义三个仪表盘的引用
const materialGauge = ref(null);
const teamGauge = ref(null);
const planGauge = ref(null);
// 定义图表实例变量
let materialChart = null;
let teamChart = null;
let planChart = null;
// 模拟的当前数据
const emergencyData = ref({
materialStock: 7.8, // 物资库存 (万件)
teamPersonnel: 125, // 队伍人数 (人)
planCompletion: 92 // 预案完备率 (%)
});
// 初始化单个仪表盘
const initGauge = (refEl, value, max, unit = '', color = '#5470C6') => {
if (!refEl.value) return;
// 销毁已存在的实例
if (refEl.value.chartInstance) {
refEl.value.chartInstance.dispose();
}
const chart = echarts.init(refEl.value);
refEl.value.chartInstance = chart;
const option = {
tooltip: {
formatter: '{a} <br/>{b} : {c}' + unit
},
series: [
{
name: '当前值',
type: 'gauge',
startAngle: 90,
endAngle: -270,
pointer: {
show: true,
width: 3,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [{
offset: 0, color: '#fff'
}, {
offset: 1, color: color
}]
}
},
progress: {
show: true,
overlap: false,
roundCap: true,
clip: false,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [{
offset: 0, color: '#36CFC9'
}, {
offset: 1, color: color
}]
}
}
},
axisLine: {
lineStyle: {
width: 8,
color: [[1, 'rgba(255, 255, 255, 0.2)']]
}
},
splitLine: {
show: true,
distance: 0,
length: 10,
lineStyle: {
color: '#fff',
width: 1
}
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
title: {
fontSize: 6,
color: '#fff'
},
detail: {
width: 60,
height: 14,
fontSize: 10,
top: 0,
color: '#fff',
borderColor: 'auto',
formatter: '{value}' + unit,
offsetCenter: [0, '-120%']
},
data: [
{
value: value,
name: ''
}
]
}
]
};
chart.setOption(option);
return chart;
};
// 初始化所有仪表盘
const initAllGauges = () => {
// 物资库存仪表盘 (最大值设为10万件)
materialChart = initGauge(materialGauge, emergencyData.value.materialStock, 10, '万件', '#5470C6');
// 队伍人数仪表盘 (最大值设为200人)
teamChart = initGauge(teamGauge, emergencyData.value.teamPersonnel, 200, '人', '#91CC75');
// 预案完备率仪表盘 (最大值设为100%)
planChart = initGauge(planGauge, emergencyData.value.planCompletion, 100, '%', '#FAC858');
};
// 更新仪表盘数据
const updateGaugeData = () => {
if (materialChart) {
materialChart.setOption({
series: [{
data: [{ value: emergencyData.value.materialStock }]
}]
});
}
if (teamChart) {
teamChart.setOption({
series: [{
data: [{ value: emergencyData.value.teamPersonnel }]
}]
});
}
if (planChart) {
planChart.setOption({
series: [{
data: [{ value: emergencyData.value.planCompletion }]
}]
});
}
};
// 监听窗口大小变化,调整图表尺寸
const handleResize = () => {
if (materialChart) materialChart.resize();
if (teamChart) teamChart.resize();
if (planChart) planChart.resize();
};
onMounted(() => {
// 初始化图表
initAllGauges();
// 添加窗口大小变化监听
window.addEventListener('resize', handleResize);
// 模拟数据更新 (每5秒更新一次)
// setInterval(() => {
// emergencyData.value.materialStock = (Math.random() * 4 + 6).toFixed(1);
// emergencyData.value.teamPersonnel = Math.floor(Math.random() * 50 + 100);
// emergencyData.value.planCompletion = Math.floor(Math.random() * 10 + 85);
// updateGaugeData();
// }, 5000);
});
onUnmounted(() => {
// 移除事件监听
window.removeEventListener('resize', handleResize);
// 销毁图表实例
if (materialChart) materialChart.dispose();
if (teamChart) teamChart.dispose();
if (planChart) planChart.dispose();
});
</script>
<style scoped>
.dashboard {
display: flex;
justify-content: space-around;
align-items: center;
width: 13vw;
height: 13vh;
margin-left: -0.5vw;
}
.gauge-item {
display: flex;
flex-direction: column;
align-items: center;
width: 30%;
/* height: 100%; */
}
.gauge-title {
font-size: calc(0.5vw);
color: #fff;
/* margin-bottom: 10px; */
text-align: center;
}
.gauge-chart {
width: 4vw;
height: 10vh;
}
</style>
// eventTypeWordCloud
<template>
<div ref="chartRef" class="chart"></div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
export default {
name: 'EventTypeBarChart',
setup() {
const chartRef = ref(null)
let chartInstance = null
let resizeHandler = null
// 事件数据(与原词云图数据一致)
const eventData = ref([
{ name: '洪水', value: 156 },
{ name: '暴雨', value: 142 },
{ name: '台风', value: 123 },
{ name: '山体滑坡', value: 98 },
{ name: '水质污染', value: 87 },
{ name: '设备故障', value: 76 },
{ name: '电力中断', value: 65 },
{ name: '交通事故', value: 54 },
{ name: '火灾', value: 43 },
{ name: '化学品泄漏', value: 32 },
{ name: '地震', value: 21 },
{ name: '疫情', value: 19 }
])
// 初始化横向条形图
const initChart = () => {
if (!chartRef.value) return
// 销毁旧实例
if (chartInstance) {
chartInstance.dispose()
}
chartInstance = echarts.init(chartRef.value)
// 颜色配置(沿用原词云图的颜色方案)
const colors = [
'#c23531', '#2f4554', '#61a0a8', '#d48265', '#91c7ae',
'#749f83', '#ca8622', '#bda29a', '#6e7074', '#546570',
'#c4ccd3', '#f05b72'
]
const option = {
// 提示框配置
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(0, 0, 0, 0.05)'
}
},
formatter: '{b}: {c}次',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#ddd',
borderWidth: 1,
textStyle: {
color: '#fff'
}
},
// 网格布局
grid: {
left: '0%', // 左侧留出更多空间显示事件名称
right: '10%',
top: '10%',
bottom: '10%',
containLabel: true
},
// Y轴(事件类型,类目轴)
yAxis: {
type: 'category',
data: eventData.value.map(item => item.name),
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
axisTick: {
show: false
},
axisLabel: {
color: '#fff',
fontSize: 8,
},
// 事件名称右对齐
align: 'right'
},
// X轴(事件数量,数值轴)
xAxis: {
type: 'value',
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
axisTick: {
show: false
},
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
axisLabel: {
color: '#fff',
fontSize: 8
}
},
// 系列配置(横向条形图)
series: [{
name: '事件数量',
type: 'bar',
data: eventData.value.map(item => item.value),
// 条形图样式
barWidth: 5,
borderRadius: [0, 4, 4, 0], // 只给右侧加圆角
// 每个条形使用不同颜色
itemStyle: {
color: (params) => colors[params.dataIndex % colors.length],
// hover时的阴影效果
emphasis: {
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
},
// 数值标签(显示在条形图右侧)
label: {
show: true,
position: 'right',
distance: 5,
color: '#fff',
fontSize: 7,
formatter: '{c}次'
}
}]
}
chartInstance.setOption(option)
}
onMounted(() => {
// 延迟初始化,确保DOM渲染完成
setTimeout(() => {
initChart()
}, 100)
// 监听窗口大小变化
resizeHandler = () => {
if (chartInstance) {
chartInstance.resize()
}
}
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
// 清理事件监听和图表实例
window.removeEventListener('resize', resizeHandler)
if (chartInstance) {
chartInstance.dispose()
}
})
return {
chartRef
}
}
}
</script>
<style lang="scss" scoped>
.chart {
width: 13vw;
height: 13vh;
margin-left: -1vw;
}
</style>
// dronePatrol
<template>
<div class="card-count">
<span>无人机巡航</span>
</div>
<div class="monitor-stats">
<div class="stat-item">
<!-- <img src="@/assets/drone/device/wait.png" alt="" class="stat-item-img" /> -->
<img src="@/assets/drone/task/01@1x.png" alt="" class="stat-item-img"/>
<div class="stat-text">
<p>机场个数:</p>
<p class="card-num">10个</p>
</div>
</div>
<div class="stat-item">
<!-- <img src="@/assets/drone/device/wait.png" alt="" class="stat-item-img" /> -->
<img src="@/assets/drone/task/04@1x.png" alt="" class="stat-item-img"/>
<div class="stat-text">
<p>巡航里程:</p>
<p class="card-num">100km</p>
</div>
</div>
<div class="stat-item">
<!-- <img src="@/assets/drone/device/wait.png" alt="" class="stat-item-img" /> -->
<img src="@/assets/drone/task/02@1x.png" alt="" class="stat-item-img"/>
<div class="stat-text">
<p>飞行时间:</p>
<p class="card-num">3h</p>
</div>
</div>
<div class="stat-item">
<!-- <img src="@/assets/drone/device/wait.png" alt="" class="stat-item-img" /> -->
<img src="@/assets/drone/task/03@1x.png" alt="" class="stat-item-img"/>
<div class="stat-text">
<p>飞行次数:</p>
<p class="card-num">6次</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.card-count {
background-image: url("@/assets/drone/device/bg01@1x.png");
background-size: 100% 100%;
background-repeat: no-repeat;
height: 2vh;
display: flex;
align-items: center;
justify-content: center;
font-size: calc(0.6vw);
img {
margin-right: 0.5vw;
}
}
.card-num {
color: #17ffe0;
font-weight: bold;
}
.stat-item {
position: relative;
height: calc(100% - 4vh);
font-size: calc(0.43vw);
width: 48%;
margin-top: 0.5vh;
}
.stat-item-img {
width: 100%;
height: 4vh;
}
.stat-text {
color: #e0efff;
display: flex;
align-items: center;
margin-left: 2vw;
position: absolute;
top: 15%;
}
.monitor-stats {
display: flex;
// height: calc(100% - 17vh);
flex-wrap: wrap;
margin-top: 0.5vh;
overflow-y: auto;
justify-content: space-between;
}
</style>
// engineering
<template>
<div class="card-menu">
<div class="menu-div">
<div class="menu-title">
合同履约率
</div>
<div class="menu-num" style="color:#FFF000">
80%
</div>
<div class="menu-img">
<img src="@/assets/drone/video/设备@1x.png" alt="" class="img-top" />
<img src="@/assets/drone/video/底座.png" alt="" class="img-footer" />
</div>
</div>
<div class="menu-div1">
<div class="menu-title">
质量合格率
</div>
<div class="menu-num" style="color:#17FFE0">
95%
</div>
<div class="menu-img">
<img src="@/assets/drone/video/在线@1x.png" alt="" class="img-top" />
<img src="@/assets/drone/video/底座.png" alt="" class="img-footer" />
</div>
</div>
<div class="menu-div2">
<div class="menu-title">
安全违规次数
</div>
<div class="menu-num" style="color:#FFFFFF">
2次
</div>
<div class="menu-img">
<img src="@/assets/drone/video/离线@1x.png" alt="" class="img-top" />
<img src="@/assets/drone/video/底座.png" alt="" class="img-footer" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.menu-div {
background: url("@/assets/drone/video/Rectangle 961.png");
background-size: 100% 100%;
width: 30%;
text-align: center;
}
.menu-div1 {
background: url("@/assets/drone/video/Rectangle 959.png");
background-size: 100% 100%;
width: 30%;
text-align: center;
}
.menu-div2 {
background: url("@/assets/drone/video/Rectangle 960.png");
background-size: 100% 100%;
width: 30%;
text-align: center;
}
.card-menu {
display: flex;
justify-content: space-between;
width: 100%;
height: 13vh;
}
.menu-title{
font-size: calc(0.5vw);
margin-top: 2vh;
}
.menu-num{
margin-top: 0.5vh;
font-size: calc(0.65vw);
}
.img-top{
width: 1.5vw;
height: 3vh;
}
.img-footer{
width: 3vw;
height: 5vh;
margin-top: -2vh;
}
</style>
// equipmentHealth
<template>
<div ref="durationChartRef" class="echart-container"></div>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue';
import * as echarts from "echarts";
const durationChartRef = ref(null);
let durationChart = null; // 图表实例
// 模拟的设备健康状态数据
const deviceHealthData = ref([{
name: '台儿庄泵站',
status: 92
}, // 健康
{
name: '二级坝泵站',
status: 85
}, // 良好
{
name: '八里湾泵站',
status: 78
}, // 一般
{
name: '东湖水库',
status: 95
} // 优秀
]);
// 初始化设备健康状态图表
const initDurationChart = () => {
if (durationChartRef.value && !durationChart) {
durationChart = echarts.init(durationChartRef.value);
// 定义颜色渐变(从绿色到浅绿色,象征健康状态)
const colorStops = [{
offset: 0,
color: "rgba(0, 200, 100, 1)"
},
{
offset: 1,
color: "rgba(100, 255, 150, 0.6)"
}
];
const option = {
// 1. 鼠标悬停提示框配置
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
backgroundColor: "rgba(10, 30, 60, 0.9)",
borderColor: "#00a8ff",
borderWidth: 1,
textStyle: {
color: "#fff"
},
// 自定义提示内容:显示站点名称和健康状态值
formatter: "{b}: {c} 分",
},
legend: {
textStyle: {
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 8
},
// top: 0,
itemWidth: 12, // 图例标记的宽度
itemHeight: 8, // 图例标记的高度
},
// 2. 图表网格布局
grid: {
top: "20%",
bottom: "25%",
left: "12%",
right: "5%"
},
// 3. X轴配置(设备站点名称)
xAxis: {
type: "category",
// 使用模拟数据中的站点名称
data: deviceHealthData.value.map((item) => item.name),
axisLine: {
lineStyle: {
color: "#00a8ff"
}
},
axisTick: {
show: false
},
axisLabel: {
color: "#fff",
fontSize: 8
},
},
// 4. Y轴配置(健康状态值,范围0-100)
yAxis: {
name: "健康状态",
type: "value",
axisLine: {
lineStyle: {
color: "#fff",
fontSize: 8
}
},
nameTextStyle: {
color: '#fff',
fontSize: 8,
},
axisTick: {
show: false
},
axisLabel: {
color: "#fff",
// 格式化Y轴标签,添加百分号
formatter: '{value} %',
fontSize: 8
},
splitLine: {
lineStyle: {
color: "rgba(0, 168, 255, 0.3)"
}
},
},
// 5. 系列数据配置(柱状图)
series: [{
name: "健康状态",
type: "bar",
// 使用模拟数据中的健康状态值
data: deviceHealthData.value.map((item) => item.status),
barWidth: 15, // 调整柱子宽度
itemStyle: {
// 应用颜色渐变
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, colorStops),
barBorderRadius: [4, 4, 0, 0], // 圆角设置
},
// 6. 鼠标悬停时柱子高亮效果
emphasis: {
itemStyle: {
// 悬停时使用更亮的渐变色
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: "rgba(50, 255, 150, 1)"
},
{
offset: 1,
color: "rgba(150, 255, 200, 0.8)"
},
]),
},
},
}],
};
durationChart.setOption(option);
}
};
// 窗口大小变化处理
const handleResize = () => {
durationChart && durationChart.resize();
};
// 生命周期管理
onMounted(() => {
initDurationChart();
window.addEventListener("resize", handleResize);
});
// 组件卸载时销毁图表
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
durationChart && durationChart.dispose();
});
</script>
<style scoped>
/* 确保图表容器有明确的宽高 */
.echart-container {
width: 13vw;
height: 13vh;
margin-left: -0.5vw;
}
</style>
// floodProtectionChart
<template>
<div ref="chartRef" class="chart"></div>
</template>
<script>
import {
ref,
onMounted,
onUnmounted
} from 'vue'
import * as echarts from 'echarts'
export default {
name: 'FloodProtectionChart',
setup() {
const chartRef = ref(null)
let chartInstance = null
const protectionData = ref({
teams: {
total: 245,
categories: [{
name: '综合防汛队伍',
value: 156
},
{
name: '专业应急队伍',
value: 89
}
]
},
materials: {
total: 38560,
categories: [{
name: '救生衣',
value: 12500
},
{
name: '沙袋',
value: 20000
},
{
name: '水泵',
value: 1560
},
{
name: '发电机',
value: 320
},
{
name: '应急照明',
value: 850
},
{
name: '其他物资',
value: 3330
}
]
}
})
const initChart = () => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['队伍数量', '物资数量'],
textStyle: {
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 8
},
itemWidth: 12, // 图例标记的宽度
itemHeight: 8, // 图例标记的高度
itemGap: 8, // 图例项之间的间距
top: 0
},
grid: {
top: '15%',
left: '0%',
right: '5%',
bottom: '10%',
containLabel: true
},
xAxis: {
type: 'value',
axisLine: {
lineStyle: {
color: '#fff'
}
},
axisLabel: {
color: '#fff',
formatter: '{value}',
fontSize: 8
},
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
},
yAxis: {
type: 'category',
data: protectionData.value.teams.categories.map(t => t.name)
.concat(protectionData.value.materials.categories.map(m => m.name)),
axisLine: {
lineStyle: {
color: '#fff'
}
},
axisLabel: {
color: '#fff',
formatter: '{value}',
fontSize: 8
},
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
},
series: [{
name: '队伍数量',
type: 'bar',
data: protectionData.value.teams.categories.map(t => t.value)
.concat(new Array(protectionData.value.materials.categories.length).fill(
0)),
itemStyle: {
color: '#5470c6'
}
},
{
name: '物资数量',
type: 'bar',
data: new Array(protectionData.value.teams.categories.length).fill(0)
.concat(protectionData.value.materials.categories.map(m => m.value)),
itemStyle: {
color: '#91cc75'
}
}
]
}
chartInstance.setOption(option)
}
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
if (chartInstance) chartInstance.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
})
return {
chartRef
}
}
}
</script>
<style scoped>
.chart {
width: 13vw;
height: 13vh;
}
</style>
// rainMonitorChart
<template>
<div ref="chartRef" class="chart"></div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
export default {
name: 'RainMonitorChart',
setup() {
const chartRef = ref(null)
let chartInstance = null
const rainData = ref({
total: 156,
categories: [
{ name: '特大暴雨', value: 3, threshold: '≥250mm', color: '#C8102E' },
{ name: '大暴雨', value: 12, threshold: '100-250mm', color: '#FF3B30' },
{ name: '暴雨', value: 25, threshold: '50-100mm', color: '#FF9500' },
{ name: '大雨', value: 38, threshold: '25-50mm', color: '#FFCC00' },
{ name: '中雨', value: 45, threshold: '10-25mm', color: '#CCFF00' },
{ name: '小雨', value: 33, threshold: '0.1-10mm', color: '#98FB98' }
]
})
const initChart = () => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value)
const option = {
// 提示框样式优化
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#fff',
borderWidth: 1,
padding: [8, 12],
textStyle: { color: '#fff' },
formatter: function(params) {
const data = rainData.value.categories.find(item => item.name === params.name);
return `雨情监测 <br/>${params.name}:${params.value}次 (${params.percent.toFixed(1)}%) <br/>阈值:${data.threshold}`;
}
},
series: [
{
name: '雨情监测',
type: 'pie',
radius: ['40%', '60%'], // 调整环形比例
center: ['50%', '50%'], // 居中布局
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 8, // 圆角更精致
borderWidth: 1
},
label: {
show: true, // 显示外部标签
position: 'outside',
formatter: '{b}: {c}', // 标签内容:名称+数值
textStyle: {
color: '#fff',
fontSize: 10
}
},
labelLine: {
show: true, // 显示引导线
length: 10,
length2: 20,
lineStyle: {
color: '#ccc'
}
},
emphasis: {
scale: true, // hover时放大效果
itemStyle: {
shadowBlur: 10, // 阴影增强立体感
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
},
data: rainData.value.categories.map(item => ({
value: item.value,
name: item.name,
itemStyle: { color: item.color }
}))
}
]
}
chartInstance.setOption(option)
}
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
if (chartInstance) chartInstance.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
})
return {
chartRef,
rainData
}
}
}
</script>
<style scoped>
.chart{
width: 13vw;
height: 13vh;
}
</style>
// waterMonitorChart
<template>
<div ref="chartRef" class="chart"></div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
export default {
name: 'WaterMonitorChart',
setup() {
const chartRef = ref(null)
let chartInstance = null
const waterData = ref({
stations: [
{ name: '长江站', warningLevel: 12.5, data: [11.2, 11.5, 11.8, 12.1, 12.4, 12.6, 12.3] },
{ name: '黄河站', warningLevel: 8.7, data: [7.2, 7.5, 7.9, 8.2, 8.5, 8.6, 8.4] },
{ name: '淮河站', warningLevel: 10.3, data: [9.1, 9.3, 9.6, 9.9, 10.1, 10.2, 10.0] }
],
times: ['06:00', '09:00', '12:00', '15:00', '18:00', '21:00', '00:00']
})
const initChart = () => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value)
const series = waterData.value.stations.map(station => ({
name: station.name,
type: 'line',
data: station.data,
markLine: {
data: [
{
yAxis: station.warningLevel,
name: '警戒水位',
lineStyle: {
color: '#ff0000',
type: 'dashed'
},
label: {
formatter: station.name + '警戒线',
position: 'insideEndTop',
fontSize:10,
}
}
]
}
}))
const option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: waterData.value.stations.map(s => s.name), // 图例数据(区域名称)
textStyle: { color: 'rgba(255, 255, 255, 0.8)', fontSize: 8 },
top: 0,
itemWidth: 12, // 图例标记的宽度
itemHeight: 8, // 图例标记的高度
},
grid: {
top: '20%',
left: '3%',
right: '10%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: waterData.value.times,
// axisLine: {
// lineStyle: {
// color: '#fff'
// }
// },
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
axisLabel: {
color: '#fff',
formatter: '{value}',
fontSize:8
},
},
yAxis: {
type: 'value',
name: '水位(m)',
nameTextStyle: { color: '#fff',fontSize: 8, },
axisLine: {
lineStyle: {
color: '#fff',
}
},
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
axisLabel: {
color: '#fff',
formatter: '{value}',
fontSize:8
},
},
series
}
chartInstance.setOption(option)
}
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
if (chartInstance) chartInstance.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
})
return {
chartRef
}
}
}
</script>
<style scoped>
.chart{
width: 13vw;
height: 13vh;
}
</style>
// safetyMonitoring
<template>
<div>
<div ref="lineChartRef" class="echart-containers"></div>
</div>
</template>
<script setup>
import * as echarts from "echarts";
import { ref, onMounted, onUnmounted } from "vue";
// 图表DOM引用
const lineChartRef = ref(null);
// 图表实例
let lineChart = null;
// 柱状图配置
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: ['渗压告警数量', '应变告警数量', '测缝告警数量', '位移告警数量'],
axisLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.2)'
}
},
axisLabel: {
color: '#fff',
fontSize:6.7,
},
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
},
yAxis: {
type: 'value',
name: '亿m³',
min: 0,
max: 500,
interval: 100,
axisLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
axisLabel: {
color: '#fff',
formatter: '{value}',
fontSize:8
},
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
},
series: [
{
name: '储存量',
type: 'bar',
data: [100, 200, 300, 400],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#2378f7' },
{ offset: 0.7, color: '#2378f7' },
{ offset: 1, color: '#83bff6' }
])
}
},
barWidth: '40%'
}
],
grid: {
left: '0%',
right: '4%',
bottom: '0%',
top: '10%',
containLabel: true
}
};
// 初始化图表
const initChart = () => {
if (lineChartRef.value) {
// 初始化echarts实例
lineChart = echarts.init(lineChartRef.value);
// 设置配置项
lineChart.setOption(option);
// 添加窗口大小变化监听,实现自适应
window.addEventListener('resize', handleResize);
}
};
// 处理窗口大小变化
const handleResize = () => {
if (lineChart) {
lineChart.resize();
}
};
// 组件挂载时初始化图表
onMounted(() => {
// 使用setTimeout确保DOM已经渲染
setTimeout(() => {
initChart();
}, 0);
});
// 组件卸载时清理资源
onUnmounted(() => {
if (lineChart) {
window.removeEventListener('resize', handleResize);
lineChart.dispose();
lineChart = null;
}
});
</script>
<style scoped>
.echart-containers {
width: 13vw;
height: 12vh;
margin-left: -0.5vw;
}
</style>
// warningStatistics
<template>
<div class="card-count">
<span>危险源总计:</span>
<span class="card-num">21</span>
</div>
<div class="monitor-stats">
<div class="stat-item">
<img src="@/assets/drone/device/wait.png" alt="" class="stat-item-img" />
<div class="stat-text">
<p>低风险:</p>
<p class="card-num">10</p>
</div>
</div>
<div class="stat-item">
<img src="@/assets/drone/device/wait.png" alt="" class="stat-item-img" />
<div class="stat-text">
<p>一般风险:</p>
<p class="card-num">2</p>
</div>
</div>
<div class="stat-item">
<img src="@/assets/drone/device/wait.png" alt="" class="stat-item-img" />
<div class="stat-text">
<p>较大风险:</p>
<p class="card-num">3</p>
</div>
</div>
<div class="stat-item">
<img src="@/assets/drone/device/wait.png" alt="" class="stat-item-img" />
<div class="stat-text">
<p>重大风险:</p>
<p class="card-num">6</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.card-count {
background-image: url("@/assets/drone/device/bg01@1x.png");
background-size: 100% 100%;
background-repeat: no-repeat;
height: 2vh;
display: flex;
align-items: center;
justify-content: center;
font-size: calc(0.6vw);
img {
margin-right: 0.5vw;
}
}
.card-num {
color: #F17079;
font-weight: bold;
}
.stat-item {
position: relative;
height: calc(100% - 4vh);
font-size: calc(0.5vw);
width: 48%;
margin-top: 0.5vh;
}
.stat-item-img {
width: 100%;
}
.stat-text {
color: #e0efff;
display: flex;
align-items: center;
margin-left: 2vw;
position: absolute;
top: 5%;
}
.monitor-stats {
display: flex;
// height: calc(100% - 17vh);
flex-wrap: wrap;
margin-top: 0.5vh;
justify-content: space-between;
}
</style>
// waterStorage
<template>
<div>
<div ref="pieChartRef" class="echart-container"></div>
</div>
</template>
<script setup>
import * as echarts from "echarts";
import { ref, onMounted, onUnmounted } from "vue";
// 图表DOM引用
const pieChartRef = ref(null);
// 图表实例
let pieChart = null;
// 饼图配置
const option = {
tooltip: {
trigger: 'item',
textStyle: {
color: '#fff' // 提示文字颜色设为白色
}
},
legend: {
left: '60%', // 放在右侧
top: 'center', // 垂直居中
orient: 'vertical', // 垂直排列
textStyle: {
color: '#fff', // 字体颜色设为白色
fontSize: 7.5 // 调小字体大小
},
itemWidth: 7.5, // 图例标记的宽度
itemHeight: 7.5, // 图例标记的高度
itemGap: 7.5 // 图例每项之间的间隔
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['30%', '70%'],
center: ['30%', '50%'], // 调整饼图位置,为右侧图例留出空间
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 20,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 1048, name: '东平湖蓄水' },
{ value: 735, name: '上级湖蓄水' },
{ value: 580, name: '下级湖蓄水' },
{ value: 484, name: '大屯水库蓄水' },
{ value: 300, name: '东湖水库蓄水' },
{ value: 300, name: '双王城水库蓄水' }
]
}
]
};
// 初始化图表
const initChart = () => {
if (pieChartRef.value) {
// 初始化echarts实例
pieChart = echarts.init(pieChartRef.value);
// 设置配置项
pieChart.setOption(option);
// 添加窗口大小变化监听,实现自适应
window.addEventListener('resize', handleResize);
}
};
// 处理窗口大小变化
const handleResize = () => {
if (pieChart) {
pieChart.resize();
}
};
// 组件挂载时初始化图表
onMounted(() => {
// 使用nextTick确保DOM已经渲染
setTimeout(() => {
initChart();
}, 0);
});
// 组件卸载时清理资源
onUnmounted(() => {
if (pieChart) {
window.removeEventListener('resize', handleResize);
pieChart.dispose();
pieChart = null;
}
});
</script>
<style scoped>
.echart-container {
width: 10vw;
height: 11vh;
/* min-height: 300px; */
}
</style>
// waterDemandChart
<template>
<div class="water-supply-stats">
<div class="chart-container">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import * as echarts from 'echarts';
// 模拟数据:年份、区域、供水量(万m³)
const supplyData = [
{ year: 2020, region: '济南', supply: 105.3 },
{ year: 2020, region: '青岛', supply: 260.8 },
{ year: 2020, region: '淄博', supply: 215.2 },
{ year: 2020, region: '枣庄', supply: 240.5 },
{ year: 2021, region: '济南', supply: 110.5 },
{ year: 2021, region: '青岛', supply: 265.2 },
{ year: 2021, region: '淄博', supply: 220.8 },
{ year: 2021, region: '枣庄', supply: 245.3 },
{ year: 2022, region: '济南', supply: 130.8 },
{ year: 2022, region: '青岛', supply: 278.5 },
{ year: 2022, region: '淄博', supply: 232.4 },
{ year: 2022, region: '枣庄', supply: 156.7 },
{ year: 2023, region: '济南', supply: 256.3 },
{ year: 2023, region: '青岛', supply: 195.8 },
{ year: 2023, region: '淄博', supply: 145.2 },
{ year: 2023, region: '枣庄', supply: 172.5 },
];
// 响应式状态:选中的年份(用于筛选总供水量)
const selectedYear = ref<number | 'all'>('all');
// 提取唯一的年份和区域
const years = Array.from(new Set(supplyData.map(item => item.year))).sort((a, b) => a - b);
const regions = Array.from(new Set(supplyData.map(item => item.region))).sort();
// 计算总供水量(根据选中的年份筛选)
const totalSupply = computed(() => {
if (selectedYear.value === 'all') {
// 所有年份的总供水量
return supplyData.reduce((sum, item) => sum + item.supply, 0).toFixed(1);
} else {
// 选中年份的总供水量
return supplyData
.filter(item => item.year === selectedYear.value)
.reduce((sum, item) => sum + item.supply, 0)
.toFixed(1);
}
});
// ECharts 实例引用
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance : echarts.ECharts | null = null;
// 初始化折线图
const initChart = () => {
if (!chartRef.value) return;
// 销毁已存在的实例
if (chartInstance) {
chartInstance.dispose();
}
// 创建新实例
chartInstance = echarts.init(chartRef.value);
// 处理图表数据:按区域分组,每个区域对应一条折线
const seriesData = regions.map(region => {
return {
name: region,
type: 'line',
smooth: true, // 平滑折线
data: years.map(year => {
const item = supplyData.find(i => i.year === year && i.region === region);
return item ? item.supply : 0;
})
};
});
// 图表配置项
const option = {
tooltip: {
trigger: 'axis', // 触发方式:按轴触发
backgroundColor: 'rgba(0, 30, 60, 0.8)',
borderColor: 'rgba(34, 197, 255, 0.8)',
textStyle: { color: '#fff', fontSize: 7 },
formatter: '{b}年<br/>{a}:{c} 万m³' // 自定义提示框格式
},
legend: {
data: regions, // 图例数据(区域名称)
textStyle: { color: 'rgba(255, 255, 255, 0.8)', fontSize: 8 },
top: 0,
itemWidth: 12, // 图例标记的宽度
itemHeight: 8, // 图例标记的高度
},
grid: {
top: '25%',
left: '3%',
right: '0%',
bottom: '2%',
containLabel: true
},
xAxis: {
type: 'category',
data: years, // x轴数据(年份)
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
axisLabel: {
color: '#fff',
fontSize: 8,
}
},
yAxis: {
type: 'value',
name: '需水量(万m³)', // y轴名称
nameTextStyle: { color: '#fff',fontSize: 8, },
axisLabel: {
color: '#fff',
fontSize: 8,
},
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } }
},
series: seriesData,
color: ['#3498db', '#2ecc71', '#e74c3c', '#f39c12'] // 折线颜色(区分不同区域)
};
// 应用配置项
chartInstance.setOption(option);
};
// 监听窗口大小变化,调整图表尺寸
const handleResize = () => {
chartInstance?.resize();
};
// 生命周期钩子
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose(); // 销毁图表实例,释放资源
});
</script>
<style scoped>
.water-supply-stats {}
.total-value {
color: #fff;
font-size: calc(0.7vw);
font-weight: 600;
}
.chart-container {
height: 12vh;
position: relative;
}
.chart {
width: 100%;
height: 100%;
}
.chart-header {
font-size: calc(0.8vw);
height: 2vh;
color: #30B5DE;
}
</style>
// waterFlowChart
<template>
<div class="custom-table-container">
<table class="custom-table">
<thead>
<tr>
<th>区域</th>
<th>时间</th>
<th>供水量</th>
</tr>
</thead>
<!-- 表体 -->
<tbody>
<tr v-for="item in drillPlans" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.time }}</td>
<td>{{ item.completionRate }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
// 表格数据
const drillPlans = ref([
{ id: 1, name: '省界调水量', time: '10-11', completionRate: 95 },
{ id: 2, name: '入下级湖水量', time: '10-13', completionRate: 100 },
{ id: 3, name: '入上级湖水量', time: '10-05', completionRate: 60 },
{ id: 4, name: '出上级湖水量', time: '10-10', completionRate: 0 },
{ id: 5, name: '入东平湖水量', time: '10-30', completionRate: 0 },
{ id: 6, name: '渠首闸水量', time: '10-30', completionRate: 0 },
{ id: 7, name: '出湖调水量', time: '10-30', completionRate: 0 },
{ id: 8, name: '南四湖进水量', time: '10-15', completionRate: 85 },
{ id: 9, name: '北四湖出水量', time: '10-20', completionRate: 70 },
{ id: 10, name: '中段调水量', time: '10-25', completionRate: 45 },
]);
</script>
<style lang="css" scoped>
/* 容器样式 */
.custom-table-container {
width: 13vw; /* 缩小后的宽度 */
height: 13vh; /* 缩小后的高度 */
margin-left: -0.5vw;
border-radius: 8px;
border: 1px solid #004a99;
overflow: hidden; /* 隐藏外部滚动 */
}
/* 表格整体样式 */
.custom-table {
width: 100%;
height: 100%;
border-collapse: collapse;
font-size: calc(0.5vw); /* 缩小后的字体大小 */
color: #ffffff;
background-color: transparent;
display: block; /* 改为块级显示 */
}
/* 表头固定 */
.custom-table thead {
display: block;
width: 100%;
background: linear-gradient(180deg, rgba(22, 117, 196, 0) 0%, rgba(22, 117, 196, 0.7) 100%);
}
/* 表头行 */
.custom-table thead tr {
display: table;
width: 100%;
table-layout: fixed;
}
/* 表头单元格 */
.custom-table th {
text-align: center;
padding: 2px 4px; /* 缩小内边距 */
border-right: 1px solid #004a99;
border-bottom: 1px solid #004a99;
line-height: 1.2; /* 缩小行高 */
}
/* 表体滚动 */
.custom-table tbody {
display: block;
height: calc(100% - 10px); /* 减去表头高度 */
overflow-y: auto;
width: 100%;
}
/* 表体行 */
.custom-table tbody tr {
display: table;
width: 100%;
table-layout: fixed;
}
/* 表体单元格 */
.custom-table td {
padding: 2px 4px; /* 缩小内边距 */
text-align: center;
border-right: 1px solid #004a99;
border-bottom: 1px solid #004a99;
line-height: 1.2; /* 缩小行高 */
white-space: nowrap; /* 防止文字换行 */
overflow: hidden; /* 隐藏超出内容 */
text-overflow: ellipsis; /* 超出部分显示省略号 */
}
/* 列宽设置 */
.custom-table th:nth-child(1),
.custom-table td:nth-child(1) {
width: 40%;
}
.custom-table th:nth-child(2),
.custom-table td:nth-child(2) {
width: 30%;
}
.custom-table th:nth-child(3),
.custom-table td:nth-child(3) {
width: 30%;
}
/* 移除最后一列的右边框 */
.custom-table th:last-child,
.custom-table td:last-child {
border-right: none;
}
/* 行 hover 效果 */
.custom-table tbody tr:hover td {
background-color: rgba(0, 74, 153, 0.3);
}
/* 自定义滚动条样式 */
.custom-table tbody::-webkit-scrollbar {
width: 6px;
}
.custom-table tbody::-webkit-scrollbar-track {
background: rgba(0, 74, 153, 0.2);
border-radius: 3px;
}
.custom-table tbody::-webkit-scrollbar-thumb {
background: rgba(0, 74, 153, 0.6);
border-radius: 3px;
}
.custom-table tbody::-webkit-scrollbar-thumb:hover {
background: rgba(0, 74, 153, 0.8);
}
</style>
// waterSupplyChart
<template>
<div class="water-supply-stats">
<div class="chart-container">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import * as echarts from 'echarts';
// 模拟数据:年份、区域、供水量(万m³)
const supplyData = [
{ year: 2020, region: '济南', supply: 205.3 },
{ year: 2020, region: '青岛', supply: 160.8 },
{ year: 2020, region: '淄博', supply: 115.2 },
{ year: 2020, region: '枣庄', supply: 140.5 },
{ year: 2021, region: '济南', supply: 210.5 },
{ year: 2021, region: '青岛', supply: 165.2 },
{ year: 2021, region: '淄博', supply: 120.8 },
{ year: 2021, region: '枣庄', supply: 145.3 },
{ year: 2022, region: '济南', supply: 230.8 },
{ year: 2022, region: '青岛', supply: 178.5 },
{ year: 2022, region: '淄博', supply: 132.4 },
{ year: 2022, region: '枣庄', supply: 156.7 },
{ year: 2023, region: '济南', supply: 256.3 },
{ year: 2023, region: '青岛', supply: 195.8 },
{ year: 2023, region: '淄博', supply: 145.2 },
{ year: 2023, region: '枣庄', supply: 172.5 },
];
// 响应式状态:选中的年份(用于筛选总供水量)
const selectedYear = ref<number | 'all'>('all');
// 提取唯一的年份和区域
const years = Array.from(new Set(supplyData.map(item => item.year))).sort((a, b) => a - b);
const regions = Array.from(new Set(supplyData.map(item => item.region))).sort();
// 计算总供水量(根据选中的年份筛选)
const totalSupply = computed(() => {
if (selectedYear.value === 'all') {
// 所有年份的总供水量
return supplyData.reduce((sum, item) => sum + item.supply, 0).toFixed(1);
} else {
// 选中年份的总供水量
return supplyData
.filter(item => item.year === selectedYear.value)
.reduce((sum, item) => sum + item.supply, 0)
.toFixed(1);
}
});
// ECharts 实例引用
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance : echarts.ECharts | null = null;
// 初始化折线图
const initChart = () => {
if (!chartRef.value) return;
// 销毁已存在的实例
if (chartInstance) {
chartInstance.dispose();
}
// 创建新实例
chartInstance = echarts.init(chartRef.value);
// 处理图表数据:按区域分组,每个区域对应一条折线
const seriesData = regions.map(region => {
return {
name: region,
type: 'line',
smooth: true, // 平滑折线
data: years.map(year => {
const item = supplyData.find(i => i.year === year && i.region === region);
return item ? item.supply : 0;
})
};
});
// 图表配置项
const option = {
tooltip: {
trigger: 'axis', // 触发方式:按轴触发
backgroundColor: 'rgba(0, 30, 60, 0.8)',
borderColor: 'rgba(34, 197, 255, 0.8)',
textStyle: { color: '#fff', fontSize: 7 },
formatter: '{b}年<br/>{a}:{c} 万m³' // 自定义提示框格式
},
legend: {
data: regions, // 图例数据(区域名称)
textStyle: { color: 'rgba(255, 255, 255, 0.8)', fontSize: 8 },
top: 0,
itemWidth: 12, // 图例标记的宽度
itemHeight: 8, // 图例标记的高度
},
grid: {
top: '25%',
left: '5%',
right: '0%',
bottom: '2%',
containLabel: true
},
xAxis: {
type: 'category',
data: years, // x轴数据(年份)
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
axisLabel: {
color: '#fff',
fontSize: 8,
}
},
yAxis: {
type: 'value',
name: '供水量(万m³)', // y轴名称
nameTextStyle: { color: '#fff',fontSize: 8, },
axisLabel: {
color: '#fff',
fontSize: 8,
},
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } }
},
series: seriesData,
color: ['#3498db', '#2ecc71', '#e74c3c', '#f39c12'] // 折线颜色(区分不同区域)
};
// 应用配置项
chartInstance.setOption(option);
};
// 监听窗口大小变化,调整图表尺寸
const handleResize = () => {
chartInstance?.resize();
};
// 生命周期钩子
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose(); // 销毁图表实例,释放资源
});
</script>
<style scoped>
.water-supply-stats {}
.total-value {
color: #fff;
font-size: calc(0.7vw);
font-weight: 600;
}
.chart-container {
height: 12vh;
position: relative;
}
.chart {
width: 100%;
height: 100%;
}
.chart-header {
font-size: calc(0.8vw);
height: 2vh;
color: #30B5DE;
}
</style>
// monitoringDataChart
<template>
<div>
<div ref="chartRef" class="chart-controls"></div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
export default {
name: 'MonitoringDataChart',
setup() {
const chartRef = ref(null)
const selectedSite = ref('site1')
const selectedIndicators = ref(['temperature', 'pH', 'DO'])
let chartInstance = null
const sites = ref([
{ id: 'site1', name: '长江监测点' },
{ id: 'site2', name: '黄河监测点' },
{ id: 'site3', name: '珠江监测点' }
])
const indicators = ref([
{ key: 'temperature', name: '水温(°C)', unit: '°C' },
{ key: 'pH', name: 'pH', unit: '' },
{ key: 'DO', name: '溶解氧(mg/L)', unit: 'mg/L' },
{ key: 'TP', name: '总磷(mg/L)', unit: 'mg/L' },
{ key: 'TN', name: '总氮(mg/L)', unit: 'mg/L' },
{ key: 'NH3N', name: '氨氮(mg/L)', unit: 'mg/L' }
])
const timeData = ref(['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'])
const monitoringData = ref({
site1: {
temperature: [12.5, 13.2, 15.8, 18.5, 22.3, 25.7, 28.9, 28.5, 25.2, 20.1, 16.3, 13.8],
pH: [7.2, 7.3, 7.1, 7.4, 7.5, 7.3, 7.2, 7.4, 7.3, 7.2, 7.1, 7.3],
DO: [8.5, 8.3, 8.7, 8.2, 7.9, 7.8, 7.5, 7.6, 7.9, 8.2, 8.4, 8.3],
TP: [0.12, 0.15, 0.18, 0.22, 0.25, 0.28, 0.31, 0.29, 0.26, 0.21, 0.17, 0.14],
TN: [1.2, 1.3, 1.5, 1.7, 1.9, 2.1, 2.3, 2.2, 2.0, 1.8, 1.5, 1.3],
NH3N: [0.35, 0.38, 0.42, 0.48, 0.52, 0.58, 0.62, 0.59, 0.53, 0.46, 0.40, 0.36]
},
site2: {
temperature: [10.8, 11.5, 14.2, 17.0, 20.8, 24.2, 27.5, 27.0, 23.5, 18.2, 14.0, 11.2],
pH: [7.5, 7.4, 7.6, 7.3, 7.2, 7.4, 7.5, 7.3, 7.4, 7.5, 7.6, 7.4],
DO: [9.2, 9.0, 8.8, 8.5, 8.2, 8.0, 7.8, 7.9, 8.1, 8.4, 8.7, 9.0],
TP: [0.08, 0.10, 0.13, 0.16, 0.19, 0.22, 0.25, 0.23, 0.20, 0.17, 0.13, 0.10],
TN: [0.9, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 1.9, 1.7, 1.5, 1.2, 1.0],
NH3N: [0.25, 0.28, 0.32, 0.38, 0.42, 0.47, 0.51, 0.48, 0.43, 0.37, 0.31, 0.27]
},
site3: {
temperature: [14.2, 15.0, 17.5, 20.2, 24.0, 27.3, 30.5, 30.0, 27.0, 22.5, 18.5, 15.5],
pH: [7.0, 7.1, 6.9, 7.2, 7.3, 7.1, 7.0, 7.2, 7.1, 7.0, 6.9, 7.1],
DO: [7.8, 7.6, 7.9, 7.5, 7.2, 7.0, 6.8, 6.9, 7.2, 7.5, 7.7, 7.6],
TP: [0.15, 0.18, 0.22, 0.26, 0.30, 0.34, 0.38, 0.35, 0.31, 0.26, 0.21, 0.17],
TN: [1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.5, 2.3, 2.1, 1.8, 1.6],
NH3N: [0.42, 0.45, 0.50, 0.56, 0.61, 0.67, 0.72, 0.68, 0.62, 0.55, 0.48, 0.43]
}
})
const initChart = () => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value)
updateChart()
}
}
const updateChart = () => {
if (!chartInstance) return
const series = selectedIndicators.value.map(indicatorKey => {
const indicator = indicators.value.find(i => i.key === indicatorKey)
return {
name: indicator.name,
type: 'line',
data: monitoringData.value[selectedSite.value][indicatorKey],
smooth: true,
symbol: 'circle',
symbolSize: 6
}
})
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
data: selectedIndicators.value.map(key => {
return indicators.value.find(i => i.key === key).name
}),
textStyle: { color: 'rgba(255, 255, 255, 0.8)', fontSize: 8 },
top: 0,
itemWidth: 12, // 图例标记的宽度
itemHeight: 8, // 图例标记的高度
},
grid: {
top: '15%',
left: '-2%',
right: '10%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: timeData.value,
axisLabel: {
color: '#fff',
fontSize: 8,
},
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
},
yAxis: {
type: 'value',
axisLabel: {
color: '#fff',
fontSize: 8,
},
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } },
},
series
}
chartInstance.setOption(option)
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
if (chartInstance) chartInstance.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
})
return {
chartRef,
sites,
indicators,
selectedSite,
selectedIndicators,
updateChart
}
}
}
</script>
<style scoped>
.chart-controls {
width: 13vw;
height: 13vh;
}
</style>
// siteComparisonRadar
<template>
<div>
<div ref="chartRef" class="chart-controls"></div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
export default {
name: 'SiteComparisonBar',
setup() {
const chartRef = ref(null)
const selectedTime = ref('7月')
const selectedSites = ref(['site1', 'site2', 'site3'])
let chartInstance = null
// 站点配置(保留原颜色)
const sites = ref([
{ id: 'site1', name: '长江监测点', color: '#5470C6' },
{ id: 'site2', name: '黄河监测点', color: '#91CC75' },
{ id: 'site3', name: '珠江监测点', color: '#FAC858' }
])
// 时间选项(不变)
const timeOptions = ref(['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'])
// 监测指标(x轴类目,保留单位)
const indicators = ref([
{ name: '水温', max: 35, unit: '°C' },
{ name: 'pH', max: 9, unit: '' },
{ name: '溶解氧', max: 12, unit: 'mg/L' },
{ name: '总磷', max: 0.5, unit: 'mg/L' },
{ name: '总氮', max: 3, unit: 'mg/L' },
{ name: '氨氮', max: 1, unit: 'mg/L' }
])
// 核心修正:radarData 是对象字面量 {},不是数组 []
const radarData = ref({
site1: {
'1月': [12.5, 7.2, 8.5, 0.12, 1.2, 0.35],
'2月': [13.2, 7.3, 8.3, 0.15, 1.3, 0.38],
'3月': [15.8, 7.1, 8.7, 0.18, 1.5, 0.42],
'4月': [18.5, 7.4, 8.2, 0.22, 1.7, 0.48],
'5月': [22.3, 7.5, 7.9, 0.25, 1.9, 0.52],
'6月': [25.7, 7.3, 7.8, 0.28, 2.1, 0.58],
'7月': [28.9, 7.2, 7.5, 0.31, 2.3, 0.62],
'8月': [28.5, 7.4, 7.6, 0.29, 2.2, 0.59],
'9月': [25.2, 7.3, 7.9, 0.26, 2.0, 0.53],
'10月': [20.1, 7.2, 8.2, 0.21, 1.8, 0.46],
'11月': [16.3, 7.1, 8.4, 0.17, 1.5, 0.40],
'12月': [13.8, 7.3, 8.3, 0.14, 1.3, 0.36]
},
site2: {
'1月': [10.8, 7.5, 9.2, 0.08, 0.9, 0.25],
'2月': [11.5, 7.4, 9.0, 0.10, 1.0, 0.28],
'3月': [14.2, 7.6, 8.8, 0.13, 1.2, 0.32],
'4月': [17.0, 7.3, 8.5, 0.16, 1.4, 0.38],
'5月': [20.8, 7.2, 8.2, 0.19, 1.6, 0.42],
'6月': [24.2, 7.4, 8.0, 0.22, 1.8, 0.47],
'7月': [27.5, 7.5, 7.8, 0.25, 2.0, 0.51],
'8月': [27.0, 7.3, 7.9, 0.23, 1.9, 0.48],
'9月': [23.5, 7.4, 8.1, 0.20, 1.7, 0.43],
'10月': [18.2, 7.5, 8.4, 0.17, 1.5, 0.37],
'11月': [14.0, 7.6, 8.7, 0.13, 1.2, 0.31],
'12月': [11.2, 7.4, 9.0, 0.10, 1.0, 0.27]
},
site3: {
'1月': [14.2, 7.0, 7.8, 0.15, 1.5, 0.42],
'2月': [15.0, 7.1, 7.6, 0.18, 1.6, 0.45],
'3月': [17.5, 6.9, 7.9, 0.22, 1.8, 0.50],
'4月': [20.2, 7.2, 7.5, 0.26, 2.0, 0.56],
'5月': [24.0, 7.3, 7.2, 0.30, 2.2, 0.61],
'6月': [27.3, 7.1, 7.0, 0.34, 2.4, 0.67],
'7月': [30.5, 7.0, 6.8, 0.38, 2.6, 0.72],
'8月': [30.0, 7.2, 6.9, 0.35, 2.5, 0.68],
'9月': [27.0, 7.1, 7.2, 0.31, 2.3, 0.62],
'10月': [22.5, 7.0, 7.5, 0.26, 2.1, 0.55],
'11月': [18.5, 6.9, 7.7, 0.21, 1.8, 0.48],
'12月': [15.5, 7.1, 7.6, 0.17, 1.6, 0.43]
}
})
// 初始化图表
const initChart = () => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value)
updateChart() // 初始加载数据
}
}
// 更新图表(核心:生成柱形图系列数据)
const updateChart = () => {
if (!chartInstance) return
// 1. 生成系列数据:每个选中的站点对应一个柱形系列
const series = selectedSites.value.map(siteId => {
const site = sites.value.find(s => s.id === siteId)
return {
name: site.name, // 系列名称(站点名,用于图例和tooltip)
type: 'bar', // 图表类型:柱形图
data: radarData.value[siteId][selectedTime.value], // 对应时间的指标数值数组
itemStyle: { color: site.color }, // 沿用原站点颜色
barWidth: 10, // 柱子宽度(可调整,避免过宽/过窄)
groupPadding: 0.2 // 同指标下不同站点柱子的间距
}
})
// 2. 柱形图配置项
const option = {
// 提示框:显示「指标名称+各站点数值(带单位)」
tooltip: {
trigger: 'axis', // 按x轴类目触发(同一指标的所有站点数据一起显示)
axisPointer: { type: 'shadow' }, // 阴影指示器(增强交互体验)
formatter: function(params) {
// params:当前x轴类目下的所有系列数据([{seriesName, value}, ...])
const indicator = indicators.value.find(ind => ind.name === params[0].axisValue)
let tooltipHtml = `<div>${indicator.name}(单位:${indicator.unit || '无'})</div>`
params.forEach(item => {
tooltipHtml += `<div>${item.seriesName}:${item.value}${indicator.unit}</div>`
})
return tooltipHtml
},
},
// 图例:显示选中的站点(支持点击切换显示/隐藏)
legend: {
data: selectedSites.value.map(id => sites.value.find(s => s.id === id).name),
top: 0,
left: 'center',
textStyle: { color: '#fff', fontSize: 8 },
itemWidth: 12,
itemHeight: 8
},
// 网格布局:调整图表边距,避免标签重叠
grid: {
top: '20%',
left: '0%',
right: '10%',
bottom: '20%', // 底部留足空间给x轴标签(防止指标名过长重叠)
containLabel: true
},
// x轴:监测指标(类目轴)
xAxis: {
type: 'category',
data: indicators.value.map(ind => ind.name), // x轴标签:指标名称
axisLabel: {
color: '#fff',
fontSize: 8,
},
// axisLine: { lineStyle: { color: 'rgba(0,0,0,0.5)' } },
axisTick: { alignWithLabel: true } // 刻度线与标签对齐
},
// y轴:数值轴(自适应数据范围)
yAxis: {
type: 'value',
name: '数值',
nameTextStyle: { color: '#fff', fontSize: 8 },
axisLabel: {
color: '#fff',
fontSize: 8,
formatter: '{value}'
},
axisLine: { lineStyle: { color: 'rgba(0,0,0,0.5)' } },
splitLine: { lineStyle: { color: 'rgba(0,0,0,0.1)' } } // 网格线样式
},
// 系列数据(前面生成的柱形系列)
series
}
chartInstance.setOption(option)
}
// 挂载时初始化图表+监听窗口 resize
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance?.resize()
})
})
// 销毁时释放图表实例
onUnmounted(() => {
chartInstance?.dispose()
})
return {
chartRef,
sites,
timeOptions,
selectedTime,
selectedSites,
updateChart // 暴露给父组件,用于切换时间/站点时更新图表
}
}
}
</script>
<style scoped>
/* 调整图表容器大小(原13vw/13vh过小,柱形图建议适当放大) */
.chart-controls {
width: 13vw; /* 可根据页面布局调整 */
height: 13vh; /* 可根据页面布局调整 */
}
</style>
// waterQualityDistribution
<template>
<div ref="chartRef" class="chart"></div>
</template>
<script>
import {
ref,
onMounted,
onUnmounted
} from 'vue'
import * as echarts from 'echarts'
export default {
name: 'WaterQualityDistribution',
setup() {
const chartRef = ref(null)
let chartInstance = null
const initChart = () => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value)
const option = {
color: ['#67F9D8', '#FFE434', '#56A3F1', '#FF917C'],
// left: 20, // 这个配置对雷达图布局没有直接影响,可以删除
legend: {
textStyle: {
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 10
},
top: 0,
left: 'left', // 可以将图例也居中,使其与图表整体协调
itemWidth: 12,
itemHeight: 8,
},
radar: [
{
indicator: [
{ text: 'I类' },
{ text: 'II类' },
{ text: 'III类' },
{ text: 'IV类' },
{ text: 'V类' }
],
// --- 核心改动:调整雷达图1的中心位置 ---
center: ['50%', '50%'], // 从 '25%' 调整到 '20%'
radius: 40,
startAngle: 10,
splitNumber: 4,
shape: 'circle',
axisName: {
formatter: '{value}',
color: '#428BD4'
},
splitArea: {
areaStyle: {
color: ['#77EADF', '#26C3BE', '#64AFE9', '#428BD4'],
shadowColor: 'rgba(0, 0, 0, 0.2)',
shadowBlur: 10
}
},
axisLine: {
lineStyle: {
color: 'rgba(211, 253, 250, 0.8)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(211, 253, 250, 0.8)'
}
}
},
{
// --- 核心改动:调整雷达图2的中心位置 ---
center: ['60%', '50%'], // 从 '75%' 调整到 '70%'
radius: 120,
axisName: {
color: '#fff',
backgroundColor: '#666',
borderRadius: 3,
padding: [3, 5]
}
}
],
series: [
{
type: 'radar',
emphasis: {
lineStyle: {
width: 4
}
},
data: [
{
value: [100, 8, 0.4, -80, 2000],
name: '站点总数'
}
]
}
]
};
chartInstance.setOption(option)
}
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
if (chartInstance) chartInstance.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
})
return {
chartRef
}
}
}
</script>
<style scoped>
.chart {
width: 13vw;
height: 13vh;
}
</style>




