You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

545 lines
20 KiB

<template>
<div class="app-container app-container-bg">
<el-card class="first-card" ref="firstCard" shadow="always">
<el-form :model="queryForm" ref="queryFormRef" :inline="true" @submit.native.prevent>
<el-form-item label="测站选择:">
<el-select v-model="queryForm.station" placeholder="测站选择" style="width: 240px;" @change="changeStation">
<el-option v-for="item in stationList" :key="item.stnmId" :label="item.name" :value="item.stnmId" />
</el-select>
</el-form-item>
<el-form-item label="起止时段:">
<el-date-picker v-model="queryForm.dateRange" type="datetimerange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD HH:mm:00">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="drawChart">查询</el-button>
</el-form-item>
</el-form>
<el-row class="info-bar">
<el-col :xs="24" :sm="24" :md="10" :lg="10" :xl="10">
<span>起报时间</span><span>{{ qbTime }}</span>
</el-col>
<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
<span>预报时段</span><span>1小时</span>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
<el-descriptions class="margin-top" :column="2" border>
<el-descriptions-item>
<template #label>
预测最高值
</template>
{{ predict.maxValue }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
预测最低值
</template>
{{ predict.minValue }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
时间
</template>
{{ predict.maxTime }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
时间
</template>
{{ predict.minTime }}
</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
</el-card>
<splitpanes class="el-card-p card-shadow carder-border mt10 pad10 default-theme container-box">
<pane size="30" min-size="30" max-size="30" style="height: 100%;">
<el-table v-table-height :data="tableData" border v-loading="loading">
<el-table-column prop="date" label="日期"></el-table-column>
<el-table-column prop="rain" label="雨量(mm)">
<template #default="scope">
<el-input v-if="isEditing" v-model="scope.row.rain" size="small" />
<span v-else>{{ scope.row.rain }}</span>
</template>
</el-table-column>
<el-table-column prop="water" label="水位(mm)">
<template #default="scope">
<el-input type="number" v-if="isEditing" v-model="scope.row.water" size="small" />
<span v-else>{{ scope.row.water }}</span>
</template>
</el-table-column>
</el-table>
</pane>
<pane size="70" min-size="70" max-size="70" class="ml10 pad20" v-loading="loading">
<div v-table-height id="main" style="width: 100%; height: 100%;"></div>
</pane>
</splitpanes>
</div>
</template>
<script setup name="Flood">
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from "echarts"
import request from "@/utils/request"
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import dayjs from 'dayjs'
// 响应式数据
const loading = ref(false)
const showSetting = ref(false)
const tableData = ref([])
const stationList = ref([])
const isEditing = ref(false)
const qbTime = ref('')
const chartInstance = ref(null)
const firstCard = ref(null)
const queryFormRef = ref(null)
// 预测值信息
const predict = reactive({
maxValue: '',
minValue: '',
maxTime: '',
minTime: ''
})
// 查询表单
const endTime = dayjs(new Date())
const startTime = endTime.subtract(10, 'hour').format('YYYY-MM-DD HH:mm:00')
const endTimeFormatted = endTime.format('YYYY-MM-DD HH:mm:00')
const queryForm = reactive({
station: null,
dateRange: [startTime, endTimeFormatted]
})
// 图表数据
const chartData = ref({})
// resize 处理函数引用
let resizeHandler = null
// 组件挂载时获取测站列表
onMounted(() => {
getStationList()
// 初始化 resize 处理函数
resizeHandler = () => {
if (chartInstance.value) {
chartInstance.value.resize()
}
}
// 添加窗口大小改变监听
window.addEventListener('resize', resizeHandler)
})
// 组件卸载时清理资源
onUnmounted(() => {
// 移除窗口大小改变监听
if (resizeHandler) {
window.removeEventListener('resize', resizeHandler)
}
// 销毁 echarts 实例
if (chartInstance.value) {
chartInstance.value.dispose()
chartInstance.value = null
}
})
// 获取测站选择下拉框数据
const getStationList = async () => {
try {
const res = await request({
url: "/history/data/getStationList",
method: 'post',
})
if (res.code == 0) {
stationList.value = res.data
queryForm.station = stationList.value[0].stnmId
drawChart()
}
} catch (error) {
console.error('获取测站列表失败:', error)
}
}
// 绘制图表
const drawChart = async () => {
loading.value = true
const params = {
startTime: queryForm.dateRange[0],
endTime: queryForm.dateRange[1],
stnmId: queryForm.station,
stationType: 'A',
stcd: ''
}
try {
const res = await request({
url: "/history/data/newHistorydata",
method: 'post',
params: params
})
if (res.code == 0) {
chartData.value = res.data
const rainData = res.data.A?.data || []
const waterData = res.data.C?.data || []
if (waterData && waterData.length > 0) {
const actualStartTime = dayjs(waterData[0][0]).format('YYYY-MM-DD HH:mm:ss')
const actualEndTime = dayjs(waterData[waterData.length - 1][0]).format('YYYY-MM-DD HH:mm:ss')
queryForm.dateRange = [actualStartTime, actualEndTime]
}
const predictData = res.data.Z?.data || []
// 表格用的水位数据,去掉最后一个点
const waterDataForTable = waterData.length > 0 ? waterData.slice(0, -1) : []
// 构建表格数据(雨量 + 水位,水位不包含最后一个点)
const tableDataValue = waterDataForTable.map(waterItem => {
const [timestamp, water] = waterItem
const rainItem = rainData.find(r => r[0] === timestamp)
const date = dayjs(timestamp).format('YYYY/MM/DD HH:mm:ss')
return {
date,
rain: rainItem ? rainItem[1]?.toString() : '',
water: water?.toString()
}
})
tableData.value = tableDataValue
// 使用 nextTick 确保 DOM 更新后再初始化图表
await nextTick()
initEcharts() // 图表仍使用完整的 waterData 和 predictData
}
} catch (error) {
console.error('获取图表数据失败:', error)
} finally {
loading.value = false
}
}
// 初始化图表
const initEcharts = () => {
// 取出A和V数据
const rainArr = chartData.value?.A?.data || []
const waterArr = chartData.value?.C?.data || []
const predictArr = chartData.value?.Z?.data || []
const filteredRain = waterArr
// 合并数据,去重相同时间点
let Xdata
if (waterArr.length > 0 && predictArr.length > 0 && waterArr[waterArr.length - 1][0] === predictArr[0][0]) {
Xdata = [...waterArr, ...predictArr.slice(1)]
} else {
Xdata = [...waterArr, ...predictArr]
}
// 生成timeData和rainfallData
const timeData = Xdata.map(item => {
// 时间戳转格式
return dayjs(item[0]).format('MM月DD日 HH:mm')
})
const timeDataLength = timeData.length
const startIndex = Math.max(0, timeDataLength - 12) // 后12条数据的起始索引
qbTime.value = dayjs(Xdata[startIndex][0]).format('YYYY-MM-DD HH:mm:ss')
// 雨量
const rainfallData = filteredRain.map(item => {
const waterItem = rainArr.find(w => w[0] === item[0])
return waterItem ? waterItem[1] : null
})
// 水位
const rainData = filteredRain.map(item => {
const waterItem = waterArr.find(w => w[0] === item[0])
return waterItem ? waterItem[1] : null
})
const levelData = rainData
// 预测水位
const waterForecast = Xdata.map(item => {
const waterItem = predictArr.find(w => w[0] === item[0])
return waterItem ? waterItem[1] : null
})
// 获取预测水位的最高值和最低值及对应时间
const validForecast = waterForecast.map((value, index) => ({ value, index })).filter(item => item.value !== null)
if (validForecast.length > 0) {
const maxItem = validForecast.reduce((max, current) =>
(current.value > max.value ? current : max), validForecast[0])
const minItem = validForecast.reduce((min, current) =>
(current.value < min.value ? current : min), validForecast[0])
predict.maxValue = maxItem.value
predict.minValue = minItem.value
predict.maxTime = timeData[maxItem.index]
predict.minTime = timeData[minItem.index]
}
// 15条横线颜色,首尾黑色,中间灰色
const splitLineColors = [
'#000', '#ccc', '#ccc', '#ccc', '#ccc',
'#ccc', '#ccc', '#ccc', '#ccc', '#ccc',
'#ccc', '#ccc', '#ccc', '#ccc', '#000'
]
const ycColor = '#1EA1CE'
function getSplitLine(index) {
return {
show: true,
lineStyle: {
color: splitLineColors,
width: 1
}
}
}
// 计算y轴的最小值和最大值,用于自动设置刻度
const yAxisRainMin = Math.min(...rainfallData.filter(v => v !== undefined && v !== null))
const yAxisRainMax = Math.max(...rainfallData.filter(v => v !== undefined && v !== null))
const allWaterData = levelData.concat(waterForecast).filter(v => v !== undefined && v !== null)
const yAxisWaterMin = allWaterData.length ? Math.min(...allWaterData) : 0
const yAxisWaterMax = allWaterData.length ? Math.max(...allWaterData) : 10
const option = {
backgroundColor: '#fff',
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
// 在 tooltip.formatter 函数中替换以下代码
formatter: function (params) {
let dataIndex = params[0].dataIndex
let html = `<div><strong>${timeData[dataIndex]}</strong></div>`
// 检查是否为预测数据区间
const isForecast = dataIndex >= filteredRain.length
// 显示雨量(仅在非预测时段或有雨量数据时显示)
if (!isForecast || rainfallData[dataIndex] !== undefined) {
html += `<div style="margin-top:4px;">
<span style="display:inline-block;width:10px;height:10px;background:#1075FD;margin-right:4px;"></span>
雨量: ${rainfallData[dataIndex] ?? '-'} mm
</div>`
}
// 显示实际水位(仅在非预测时段显示)
if (!isForecast) {
html += `<div>
<span style="display:inline-block;width:10px;height:10px;background:#EE6666;margin-right:4px;"></span>
水位: ${levelData[dataIndex] ?? '-'} m
</div>`
}
// 显示预测水位(在预测时段显示)
if (isForecast || waterForecast[dataIndex] !== null) {
html += `<div>
<span style="display:inline-block;width:10px;height:10px;background:#6f46fd;margin-right:4px;"></span>
预测水位: ${waterForecast[dataIndex] ?? '-'} m
</div>`
}
return html
}
},
legend: {
data: ['雨量', '水位', '预测水位'],
top: 20
},
toolbox: {
show: true,
feature: {
saveAsImage: { show: true, title: '下载图片' },
},
right: 30,
top: 20
},
grid: {
left: 60,
right: 60,
top: '15%',
height: '60%' // 调整高度以适应dataZoom
},
xAxis: {
type: 'category',
data: timeData,
axisLabel: {
rotate: 0,
formatter: function (value) {
const [date, time] = value.split(' ')
return `${time}\n${date}`
}
}
},
yAxis: [
{ // 雨量 - 左侧
type: 'value',
axisLine: {
show: true
},
name: '雨量 (mm)',
nameLocation: 'middle',
nameGap: 35,
inverse: true,
min: Math.floor(yAxisRainMin),
max: Math.ceil(yAxisRainMax),
axisLabel: { color: '#1075FD' },
nameTextStyle: { color: '#1075FD', fontWeight: 'bold' },
splitLine: getSplitLine(0),
},
{ // 水位 - 右侧
type: 'value',
axisLine: {
show: true
},
name: '水位 (m)',
nameLocation: 'middle',
nameGap: 45,
min: Math.floor(yAxisWaterMin),
max: Math.ceil(yAxisWaterMax),
position: 'right',
axisLabel: { color: '#EE6666' },
nameTextStyle: { color: '#EE6666', fontWeight: 'bold' },
splitLine: getSplitLine(1),
}
],
dataZoom: [
{
type: 'inside', // 内置型数据区域缩放组件
xAxisIndex: [0], // 控制第一个x轴
start: 0,
end: 100
},
{
type: 'slider', // 滑动条型数据区域缩放组件
xAxisIndex: [0],
start: 0,
end: 100,
bottom: 50, // 距离底部距离
height: 30, // 高度
fillerColor: 'rgba(167,183,204,0.4)', // 选中区域的背景色
borderColor: '#ddd',
textStyle: {
color: '#999'
},
handleSize: '100%', // 手柄大小
handleStyle: {
color: '#a7b7cc'
}
}
],
series: [
{
name: '雨量',
type: 'bar',
yAxisIndex: 0, // 使用左侧y轴
data: rainfallData,
barWidth: 10,
z: 10,
lineStyle: { color: '#1075FD', width: 3 },
itemStyle: { color: '#1075FD' },
markLine: {
symbol: 'none',
label: {
show: false
},
lineStyle: {
color: ycColor,
type: "dashed",
width: 2
},
z: 20,
data: [
{
xAxis: timeData.slice(startIndex)[0]
}
]
}
},
{
name: '水位',
type: 'line',
yAxisIndex: 1, // 使用右侧y轴
data: levelData,
symbol: 'circle',
lineStyle: { color: '#EE6666', width: 3 },
itemStyle: { color: '#EE6666' },
markLine: {
symbol: 'none',
label: {
show: false
},
lineStyle: {
color: ycColor,
type: "dashed",
width: 2
},
data: [
{
xAxis: timeData.slice(startIndex)[0]
}
]
}
},
{
name: '预测水位',
type: 'line',
yAxisIndex: 1, // 使用右侧y轴
data: waterForecast,
symbol: 'circle',
lineStyle: { color: '#6f46fd', width: 2 },
itemStyle: { color: '#6f46fd' },
markLine: {
symbol: 'none',
label: {
show: false
},
lineStyle: {
color: ycColor,
type: "dashed",
width: 2
},
data: [
{
xAxis: timeData.slice(startIndex)[0]
}
]
}
}
]
}
const chartDom = document.getElementById('main')
if (!chartDom) return
// 销毁已存在的实例
if (chartInstance.value) {
chartInstance.value.dispose()
}
// 初始化新的 echarts 实例
chartInstance.value = echarts.init(chartDom)
chartInstance.value.setOption(option)
}
// 改变下拉框数据
const changeStation = (val) => {
queryForm.station = val
drawChart()
}
</script>
<style scoped>
/* 如果需要添加样式可以在这里添加 */
</style>