614 lines
23 KiB
HTML
614 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>水质数据查询系统</title>
|
|
<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script> -->
|
|
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
|
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
<script src="https://unpkg.com/echarts@5.4.0/dist/echarts.min.js"></script>
|
|
<style>
|
|
:root {
|
|
--primary-color: #1e88e5;
|
|
--secondary-color: #e3f2fd;
|
|
--text-color: #333;
|
|
--light-gray: #f5f5f5;
|
|
--border-color: #ddd;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
|
|
body {
|
|
background-color: #f9f9f9;
|
|
color: var(--text-color);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
header {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
padding: 20px 0;
|
|
margin-bottom: 30px;
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
header h1 {
|
|
text-align: center;
|
|
font-weight: 300;
|
|
font-size: 2.5rem;
|
|
}
|
|
|
|
.card {
|
|
background-color: white;
|
|
border-radius: 5px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.query-panel {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
select, input {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
button {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
button:hover {
|
|
background-color: #0d47a1;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid var(--border-color);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.tab {
|
|
padding: 10px 20px;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
}
|
|
|
|
.tab.active {
|
|
border-bottom: 2px solid var(--primary-color);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
th, td {
|
|
padding: 12px 15px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
th {
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
tr:nth-child(even) {
|
|
background-color: var(--light-gray);
|
|
}
|
|
|
|
.chart-container {
|
|
height: 400px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.statistics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.stat-card {
|
|
background-color: white;
|
|
border-radius: 5px;
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
padding: 15px;
|
|
}
|
|
|
|
.stat-card h3 {
|
|
margin-bottom: 10px;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.stat-value {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 30px;
|
|
font-size: 18px;
|
|
color: #666;
|
|
}
|
|
|
|
.error {
|
|
color: #d32f2f;
|
|
padding: 10px;
|
|
background-color: #ffebee;
|
|
border-radius: 4px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.query-panel {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-group {
|
|
width: 100%;
|
|
}
|
|
|
|
.statistics-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="container">
|
|
<h1>水质数据查询系统</h1>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="container" id="app">
|
|
<div class="card">
|
|
<div class="query-panel">
|
|
<div class="form-group">
|
|
<label for="queryType">查询类型</label>
|
|
<select id="queryType" v-model="queryType">
|
|
<option value="river">按河道查询</option>
|
|
<option value="section">按断面查询</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group" v-if="queryType === 'river'">
|
|
<label for="river">选择河道</label>
|
|
<select id="river" v-model="selectedRiverId">
|
|
<option value="">请选择河道</option>
|
|
<option v-for="river in rivers" :key="river.riverId" :value="river.riverId">
|
|
{{ river.riverName }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group" v-if="queryType === 'section'">
|
|
<label for="section">选择断面</label>
|
|
<select id="section" v-model="selectedSectionId">
|
|
<option value="">请选择断面</option>
|
|
<option v-for="section in sections" :key="section.sectionId" :value="section.sectionId">
|
|
{{ section.sectionName }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="startDate">开始日期</label>
|
|
<input type="date" id="startDate" v-model="startDate">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="endDate">结束日期</label>
|
|
<input type="date" id="endDate" v-model="endDate">
|
|
</div>
|
|
|
|
<div class="form-group" style="align-self: flex-end;">
|
|
<button @click="queryData">查询数据</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="error" class="error">
|
|
{{ error }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" v-if="loading">
|
|
<div class="loading">数据加载中...</div>
|
|
</div>
|
|
|
|
<div class="card" v-if="!loading && waterQualityData.length > 0">
|
|
<div class="tabs">
|
|
<div class="tab" :class="{ active: activeTab === 'table' }" @click="activeTab = 'table'">
|
|
表格数据
|
|
</div>
|
|
<div class="tab" :class="{ active: activeTab === 'chart' }" @click="activeTab = 'chart'">
|
|
图表分析
|
|
</div>
|
|
<div class="tab" :class="{ active: activeTab === 'statistics' }" @click="activeTab = 'statistics'; fetchStatistics()">
|
|
统计分析
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-content" :class="{ active: activeTab === 'table' }">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>日期</th>
|
|
<th>水温 (°C)</th>
|
|
<th>pH值</th>
|
|
<th>溶解氧 (mg/L)</th>
|
|
<th>电导率 (μS/cm)</th>
|
|
<th>浊度 (NTU)</th>
|
|
<th>高锰酸盐指数 (mg/L)</th>
|
|
<th>氨氮 (mg/L)</th>
|
|
<th>总磷 (mg/L)</th>
|
|
<th>总氮 (mg/L)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="item in waterQualityData" :key="item.dataId">
|
|
<td>{{ formatDate(item.dataDate) }}</td>
|
|
<td>{{ item.temperature }}</td>
|
|
<td>{{ item.ph }}</td>
|
|
<td>{{ item.dissolvedOxygen }}</td>
|
|
<td>{{ item.conductivity }}</td>
|
|
<td>{{ item.ntu }}</td>
|
|
<td>{{ item.permanganateIndex }}</td>
|
|
<td>{{ item.ammoniaNitrogen }}</td>
|
|
<td>{{ item.totalPhosphorus }}</td>
|
|
<td>{{ item.totalNitrogen }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="tab-content" :class="{ active: activeTab === 'chart' }">
|
|
<select v-model="selectedParameter" @change="updateChart">
|
|
<option value="temperature">水温</option>
|
|
<option value="ph">pH值</option>
|
|
<option value="dissolvedOxygen">溶解氧</option>
|
|
<option value="conductivity">电导率</option>
|
|
<option value="ntu">浊度</option>
|
|
<option value="permanganateIndex">高锰酸盐指数</option>
|
|
<option value="ammoniaNitrogen">氨氮</option>
|
|
<option value="totalPhosphorus">总磷</option>
|
|
<option value="totalNitrogen">总氮</option>
|
|
</select>
|
|
<div id="chart" class="chart-container"></div>
|
|
</div>
|
|
|
|
<div class="tab-content" :class="{ active: activeTab === 'statistics' }">
|
|
<div v-if="loadingStatistics" class="loading">统计数据加载中...</div>
|
|
<div v-else class="statistics-grid">
|
|
<div class="stat-card" v-for="stat in statistics" :key="stat.parameter">
|
|
<h3>{{ getParameterDisplayName(stat.parameter) }}</h3>
|
|
<div class="stat-value">
|
|
<span class="stat-label">平均值:</span>
|
|
<span>{{ stat.average }}</span>
|
|
</div>
|
|
<div class="stat-value">
|
|
<span class="stat-label">最小值:</span>
|
|
<span>{{ stat.min }}</span>
|
|
</div>
|
|
<div class="stat-value">
|
|
<span class="stat-label">最大值:</span>
|
|
<span>{{ stat.max }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" v-if="!loading && waterQualityData.length === 0 && !error">
|
|
<p>暂无数据。请调整查询条件后重试。</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
new Vue({
|
|
el: '#app',
|
|
data: {
|
|
queryType: 'river',
|
|
rivers: [],
|
|
sections: [],
|
|
selectedRiverId: '',
|
|
selectedSectionId: '',
|
|
startDate: '',
|
|
endDate: '',
|
|
waterQualityData: [],
|
|
statistics: [],
|
|
loading: false,
|
|
loadingStatistics: false,
|
|
error: '',
|
|
activeTab: 'table',
|
|
selectedParameter: 'temperature',
|
|
chart: null
|
|
},
|
|
created() {
|
|
this.fetchRivers();
|
|
this.fetchSections();
|
|
|
|
// 设置默认日期范围为过去30天
|
|
const today = new Date();
|
|
const thirtyDaysAgo = new Date();
|
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
|
|
|
this.endDate = this.formatDateForInput(today);
|
|
this.startDate = this.formatDateForInput(thirtyDaysAgo);
|
|
},
|
|
methods: {
|
|
fetchRivers() {
|
|
axios.get('/api/rivers')
|
|
.then(response => {
|
|
this.rivers = response.data;
|
|
})
|
|
.catch(error => {
|
|
console.error('获取河道数据失败:', error);
|
|
this.error = '获取河道数据失败,请稍后重试';
|
|
});
|
|
},
|
|
fetchSections() {
|
|
axios.get('/api/sections')
|
|
.then(response => {
|
|
this.sections = response.data;
|
|
})
|
|
.catch(error => {
|
|
console.error('获取断面数据失败:', error);
|
|
this.error = '获取断面数据失败,请稍后重试';
|
|
});
|
|
},
|
|
queryData() {
|
|
this.error = '';
|
|
|
|
if (this.queryType === 'river' && !this.selectedRiverId) {
|
|
this.error = '请选择一个河道';
|
|
return;
|
|
}
|
|
|
|
if (this.queryType === 'section' && !this.selectedSectionId) {
|
|
this.error = '请选择一个断面';
|
|
return;
|
|
}
|
|
|
|
if (!this.startDate || !this.endDate) {
|
|
this.error = '请选择开始和结束日期';
|
|
return;
|
|
}
|
|
|
|
const startDateObj = new Date(this.startDate);
|
|
const endDateObj = new Date(this.endDate);
|
|
|
|
if (startDateObj > endDateObj) {
|
|
this.error = '开始日期不能晚于结束日期';
|
|
return;
|
|
}
|
|
|
|
this.loading = true;
|
|
let url;
|
|
|
|
if (this.queryType === 'river') {
|
|
url = `/api/water-quality/river/${this.selectedRiverId}/date-range?startDate=${this.startDate}&endDate=${this.endDate}`;
|
|
} else {
|
|
url = `/api/water-quality/section/${this.selectedSectionId}/date-range?startDate=${this.startDate}&endDate=${this.endDate}`;
|
|
}
|
|
|
|
axios.get(url)
|
|
.then(response => {
|
|
this.waterQualityData = response.data;
|
|
this.loading = false;
|
|
|
|
if (this.waterQualityData.length > 0) {
|
|
// 按日期排序
|
|
this.waterQualityData.sort((a, b) => new Date(a.dataDate) - new Date(b.dataDate));
|
|
|
|
// 如果在图表选项卡,更新图表
|
|
if (this.activeTab === 'chart') {
|
|
this.$nextTick(() => {
|
|
this.initChart();
|
|
this.updateChart();
|
|
});
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('查询数据失败:', error);
|
|
this.error = '查询数据失败,请稍后重试';
|
|
this.loading = false;
|
|
});
|
|
},
|
|
fetchStatistics() {
|
|
if (this.statistics.length > 0 || this.loadingStatistics) {
|
|
return; // 已有数据或正在加载中,不重复请求
|
|
}
|
|
|
|
this.loadingStatistics = true;
|
|
let url;
|
|
|
|
if (this.queryType === 'river') {
|
|
url = `/api/water-quality/river/${this.selectedRiverId}/statistics`;
|
|
} else {
|
|
url = `/api/water-quality/section/${this.selectedSectionId}/statistics`;
|
|
}
|
|
|
|
axios.get(url)
|
|
.then(response => {
|
|
this.statistics = response.data;
|
|
this.loadingStatistics = false;
|
|
})
|
|
.catch(error => {
|
|
console.error('获取统计数据失败:', error);
|
|
this.error = '获取统计数据失败,请稍后重试';
|
|
this.loadingStatistics = false;
|
|
});
|
|
},
|
|
formatDate(dateString) {
|
|
if (!dateString) return '';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('zh-CN');
|
|
},
|
|
formatDateForInput(date) {
|
|
if (!date) return '';
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
},
|
|
initChart() {
|
|
if (!this.chart) {
|
|
this.chart = echarts.init(document.getElementById('chart'));
|
|
}
|
|
},
|
|
updateChart() {
|
|
if (!this.chart || this.waterQualityData.length === 0) return;
|
|
|
|
const dates = this.waterQualityData.map(item => this.formatDate(item.dataDate));
|
|
const values = this.waterQualityData.map(item => item[this.selectedParameter]);
|
|
|
|
const option = {
|
|
title: {
|
|
text: this.getParameterDisplayName(this.selectedParameter) + '变化趋势',
|
|
left: 'center'
|
|
},
|
|
tooltip: {
|
|
trigger: 'axis'
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: dates,
|
|
axisLabel: {
|
|
rotate: 45
|
|
}
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
name: this.getParameterUnit(this.selectedParameter)
|
|
},
|
|
series: [{
|
|
name: this.getParameterDisplayName(this.selectedParameter),
|
|
type: 'line',
|
|
data: values,
|
|
smooth: true,
|
|
markPoint: {
|
|
data: [
|
|
{ type: 'max', name: '最大值' },
|
|
{ type: 'min', name: '最小值' }
|
|
]
|
|
},
|
|
markLine: {
|
|
data: [
|
|
{ type: 'average', name: '平均值' }
|
|
]
|
|
}
|
|
}]
|
|
};
|
|
|
|
this.chart.setOption(option);
|
|
},
|
|
getParameterDisplayName(parameter) {
|
|
const nameMap = {
|
|
'temperature': '水温',
|
|
'ph': 'pH值',
|
|
'dissolvedOxygen': '溶解氧',
|
|
'conductivity': '电导率',
|
|
'ntu': '浊度',
|
|
'permanganateIndex': '高锰酸盐指数',
|
|
'ammoniaNitrogen': '氨氮',
|
|
'totalPhosphorus': '总磷',
|
|
'totalNitrogen': '总氮',
|
|
'Temperature': '水温',
|
|
'PH': 'pH值',
|
|
'Dissolved Oxygen': '溶解氧',
|
|
'Conductivity': '电导率',
|
|
'NTU': '浊度',
|
|
'Permanganate Index': '高锰酸盐指数',
|
|
'Ammonia Nitrogen': '氨氮',
|
|
'Total Phosphorus': '总磷',
|
|
'Total Nitrogen': '总氮'
|
|
};
|
|
return nameMap[parameter] || parameter;
|
|
},
|
|
getParameterUnit(parameter) {
|
|
const unitMap = {
|
|
'temperature': '°C',
|
|
'ph': '',
|
|
'dissolvedOxygen': 'mg/L',
|
|
'conductivity': 'μS/cm',
|
|
'ntu': 'NTU',
|
|
'permanganateIndex': 'mg/L',
|
|
'ammoniaNitrogen': 'mg/L',
|
|
'totalPhosphorus': 'mg/L',
|
|
'totalNitrogen': 'mg/L'
|
|
};
|
|
return unitMap[parameter] || '';
|
|
}
|
|
},
|
|
watch: {
|
|
activeTab(newTab) {
|
|
if (newTab === 'chart' && this.waterQualityData.length > 0) {
|
|
this.$nextTick(() => {
|
|
this.initChart();
|
|
this.updateChart();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |