feat(): 对全天候的雨情监测开发

qzc-dev
李神峰 2026-01-29 17:58:01 +08:00
parent b57adda8e5
commit 84016faa09
12 changed files with 1043 additions and 77 deletions

View File

@ -285,7 +285,19 @@ input:-webkit-autofill:active {
th { color: #fff; }
td { color: rgba(255, 255, 255, 0.8); }
}
.ant-picker-time-panel-cell-inner{
color: #fff!important;
}
.ant-picker-time-panel-column > li.ant-picker-time-panel-cell-selected .ant-picker-time-panel-cell-inner{
background: #00a0e9;
}
.ant-picker-time-panel-column > li.ant-picker-time-panel-cell .ant-picker-time-panel-cell-inner:hover{
background: #013056 !important;
}
.ant-picker-cell {
color: rgba(255, 255, 255, 0.5); // Default inactive color

View File

@ -7,6 +7,10 @@ const apiurl = {
router: service + '/getRouters',
role: service + '/system/menu/list'
},
station: {
rainlist: service + '/real/rain/list',//雨量站
reservoirlist: service + '/reservoir/water/listV2',//水库水位站
},
sq: {
qfg: {
info:service + '/attResBase/list'
@ -23,12 +27,22 @@ const apiurl = {
},
qth: {
rainList: {
list:service + '/attResBase/rainBasinDivision/queryStPptnDetails/list',
list: service + '/attResBase/rainBasinDivision/queryStPptnDetails/list',
queryStPptnDetails: service + '/attResBase/rainBasinDivision/queryStPptnDetails/stcd', //实时雨量近几小时数据
tableList: service + '/attResBase/rainBasinDivision/queryStStbprpPerHour/StcdAndStartTimeAndEndTime',//小时历史雨量表格数据
chartList: service + '/attResBase/rainBasinDivision/queryStStbprpPerHourChart/StcdAndStartTimeAndEndTime',//小时历史雨量图数据
dayTableList: service + '/attResBase/rainBasinDivision/queryStStbprpPerDay/StcdAndStartTimeAndEndTime',//日历史雨量表格数据
dayChartList: service + '/attResBase/rainBasinDivision/queryStStbprpPerDayChart/StcdAndStartTimeAndEndTime',//日小时历史雨量图数据
nearbyHistory:service + '/attResBase/maxRain' //获取历史近几小时数据
},
reservoir: {
list:service + '/screen/monitoring/rsvr'
}
},
qzq: {
list: service + '/projectEvents/doc/page',
export: service + '/projectEvents/export'
export: service + '/projectEvents/export',
info :service + '/wholeCycle/get'
}
}
}

37
src/service/station.js Normal file
View File

@ -0,0 +1,37 @@
import apiurl from "./apiurl";
import { httpget, httppost } from "@/utils/request";
import {message} from 'antd';
//雨情列表
export async function rainlist(params) {
const {data, code, msg} = await httppost(apiurl.station.rainlist, params) || {};
if (code !== 200) {
message.error(msg || '请求失败');
return [];
}
const mapData = data.map(i=>({
...i,
lgtd : Number(i.lgtd)-1,
lttd : Number(i.lttd)-1.2,
}))
return mapData||[];
}
//水库列表
export async function reservoirlist(params) {
const {data, code, msg} = await httppost(apiurl.station.reservoirlist, params) || {};
if (code !== 200) {
message.error(msg || '请求失败');
return [];
}
const mapData = data.map(i=>({
...i,
lgtd : Number(i.lgtd)-1,
lttd : Number(i.lttd)-1.2,
}))
return mapData||[];
}

View File

@ -1,14 +1,19 @@
import React,{useState,useEffect} from 'react';
import React, { useState, useEffect } from 'react';
import { Table } from 'antd';
import CommonModal from '@/views/Home/components/UI/CommonModal';
import arrowIcon from '@/assets/images/card/arrow.png';
import smallCard from '@/assets/images/card/smallCard.png';
import wrj from '@/assets/images/card/wrj.png';
import apiurl from '@/service/apiurl';
import { httpget,httppost } from '@/utils/request';
import { httpget, httppost } from '@/utils/request';
import './index.less';
import RightPanel from '../ModalComponents/AllWeatherModal/RainMonitor/RightPanel';
const AllWeatherControl = () => {
const [reservoirItem, setReservoirItem] = useState({})
const [rainList, setRainList] = useState([])
const [detailVisible, setDetailVisible] = useState(false)
const [selectedStcd, setSelectedStcd] = useState(null)
const rainColumns = [
{
title: '站名',
@ -16,7 +21,7 @@ const AllWeatherControl = () => {
key: 'stnm',
align: 'center',
width: 140,
ellipsis:true
ellipsis: true
},
{
title: '今日',
@ -41,17 +46,28 @@ const AllWeatherControl = () => {
// Reservoir Data
const reservoirData = [
{ label: '主坝坝前', value: '103.17', unit: 'm', showArrow: true, underline: true },
{ label: '汛限水位', value: '104.50', unit: 'm' },
{ label: '距汛限', value: '-1.67', unit: 'm', isNegative: true },
{ label: '副坝坝前', value: '104.17', unit: 'm' },
{ label: '当前库容', value: '3867.0', unit: '万m³'},
{ label: '有效库容', value: '2867.0', unit: '万m³'},
{ label: '主坝坝前', value: reservoirItem?.rz, unit: 'm', upArrow: reservoirItem?.status > 0, underline: true,downArrow:reservoirItem?.status < 0 },
{ label: '汛限水位', value: reservoirItem?.flLowLimLev, unit: 'm' },
{ label: '距汛限', value: reservoirItem?.gapFlLowLimLev, unit: 'm', isNegative: reservoirItem?.gapFlLowLimLev < 0 },
{ label: '副坝坝前', value: reservoirItem?.rz, unit: 'm' },
{ label: '当前库容', value: reservoirItem?.nowCap, unit: '万m³' },
{ label: '有效库容', value: reservoirItem?.effectiveCap, unit: '万m³' },
];
//指定水库获取
const getReservoir = async () => {
try {
const result = await httpget(apiurl.sq.qth.reservoir.list);
if (result.code == 200) {
setReservoirItem(result.data[0])
}
} catch (error) {
console.log(error);
}
}
const getRainList = async () => {
try {
const result = await httpget(apiurl.sq.qth.rainList);
const result = await httpget(apiurl.sq.qth.rainList.list);
if (result.code == 200) {
setRainList(result.data)
}
@ -61,8 +77,9 @@ const AllWeatherControl = () => {
}
useEffect(() => {
getRainList()
getReservoir()
}, [])
return (
<div className="all-weather-control">
{/* 雨情 Section */}
@ -81,31 +98,47 @@ const AllWeatherControl = () => {
rowKey={'stcd'}
rowClassName={(record) => record.isTotal ? 'total-row' : ''}
bordered={false}
scroll={{y:300}}
scroll={{ y: 300 }}
onRow={(record) => ({
onClick: () => {
setSelectedStcd(record);
setDetailVisible(true);
}
})}
/>
<CommonModal
title={selectedStcd?.stnm}
visible={detailVisible}
onClose={() => setDetailVisible(false)}
width={'70%'}
bodyStyle={{ background: 'transparent', padding: 12 }}
>
<RightPanel stcd={selectedStcd?.stcd} cleanMode={true} />
</CommonModal>
</div>
{/* 水库水情 Section */}
<div className="section reservoir-section">
<div className="section-header">
<div className="title-wrapper">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span className="section-title">水库水情</span>
</div>
<div className="title-wrapper">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span className="section-title">水库水情</span>
</div>
</div>
<div className="reservoir-cards">
{reservoirData.map((item, index) => (
<div
key={index}
className="reservoir-card"
style={{ backgroundImage: `url(${smallCard})` }}
<div
key={index}
className="reservoir-card"
style={{ backgroundImage: `url(${smallCard})` }}
>
<div className={`value ${item.isPrimary ? 'primary' : ''} ${item.isNegative ? 'negative' : ''}`}>
<div className={`value ${item.isPrimary ? 'primary' : ''} ${item.isNegative ? 'negative' : 'positive'}`}>
<span className={`num ${item.underline ? 'underline' : ''}`}>{item.value}</span>
<span className="unit">{item.unit}</span>
</div>
<div className="label">{item.label}</div>
{item.showArrow && <span className="arrow-up"></span>}
{item.upArrow && <span className="arrow-up"></span>}
{item.downArrow && <span className="arrow-down"></span>}
</div>
))}
</div>
@ -114,15 +147,15 @@ const AllWeatherControl = () => {
{/* 无人机 Section */}
<div className="section uav-section">
<div className="section-header">
<div className="title-wrapper">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span className="section-title">无人机</span>
</div>
<span className="link">视频墙</span>
<div className="title-wrapper">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span className="section-title">无人机</span>
</div>
<span className="link">视频墙</span>
</div>
<div className="uav-content">
<div
className="uav-image"
<div
className="uav-image"
style={{ backgroundImage: `url(${wrj})` }}
/>
<div className="uav-actions">

View File

@ -148,12 +148,15 @@
margin-top: 2px;
}
.arrow-up {
.arrow-up,.arrow-down {
color: #ff4d4f;
position: absolute;
right: 5px;
top: 5px;
}
.arrow-down{
color: #68c639;
}
}
}
}

View File

@ -1,17 +1,66 @@
import React from 'react';
import React,{useState,useEffect} from 'react';
import smallCard from '@/assets/images/card/smallCard.png';
import apiurl from '@/service/apiurl';
import { httpget } from '@/utils/request';
import PdfView from '@/views/Home/components/UI/PdfView';
import './index.less';
const ManagementCycle = () => {
const [info, setInfo] = useState({})
const [pdfVisible, setPdfVisible] = useState(false);
const [pdfConfig, setPdfConfig] = useState({ title: '', url: '', fileId: '' });
const data = [
{ label: '安全鉴定', value: '三类坝' },
{ label: '病险水库', value: '是' },
{ label: '除险加固', value: '2024年11月' },
{ label: '降等报废', value: '无' },
{ label: '调度规则', value: '2023年12月', underline: true },
{ label: '应急预案', value: '2023年12月', underline: true },
{ label: '安全鉴定', value: info?.identifyType },
{ label: '病险水库', value: info?.isDanger },
{ label: '除险加固', value: info?.startDate },
{ label: '降等报废', value: info?.implementationMeasure },
{
label: '调度规则',
value: info?.dispatchTime,
underline: true,
clickable: true,
fileId:info?.dispatchFileIds?.length? info?.dispatchFileIds[0] + '':undefined // Assuming this field exists
},
{
label: '应急预案',
value: info?.emergencyTime,
underline: true,
clickable: true,
fileId:info?.emergencyFileIds?.length? info?.emergencyFileIds[0] + '':undefined // Assuming this field exists
},
];
const handleItemClick = (item) => {
const url = '/gunshiApp/ss/resPlanB/file/download/';
// if (!item?.dispatchFileIds || item?.dispatchFileIds.length) return;
// const field = item.label == '调度规程' ? item?.dispatchFileIds[0] + '' :
// item?.emergencyFileIds[0] + ''
if (item.clickable) {
setPdfConfig({
title: item.label,
url,
fileId:item.fileId
});
setPdfVisible(true);
}
};
const getInfo = async () => {
try {
const result = await httpget(apiurl.sq.qzq.info)
if (result.code == 200) {
setInfo(result.data)
}
} catch (error) {
console.log(error);
}
}
useEffect(() => {
getInfo()
}, [])
return (
<div className="management-cycle">
<div className="card-grid">
@ -19,7 +68,11 @@ const ManagementCycle = () => {
<div
key={index}
className="cycle-card"
style={{ backgroundImage: `url(${smallCard})` }}
style={{
backgroundImage: `url(${smallCard})`,
cursor: item.clickable ? 'pointer' : 'default'
}}
onClick={() => handleItemClick(item)}
>
<div className={`value-wrapper ${item.underline ? 'underlined' : ''}`}>
<span className="value">{item.value}</span>
@ -28,6 +81,14 @@ const ManagementCycle = () => {
</div>
))}
</div>
<PdfView
visible={pdfVisible}
title={pdfConfig.title}
url={'/gunshiApp/ss/resPlanB/file/download'}
fileId={pdfConfig.fileId}
onClose={() => setPdfVisible(false)}
/>
</div>
);
};

View File

@ -0,0 +1,235 @@
import React, { useEffect, useMemo, useState } from 'react';
import { DatePicker, Button, Table } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import ReactEcharts from 'echarts-for-react';
import apiurl from '@/service/apiurl';
import { httpget, httppost } from '@/utils/request';
import moment from 'moment';
import drpOption from './drpOption';
import './index.less';
const { RangePicker } = DatePicker;
export default function RightPanel({ stcd, cleanMode = false }) {
const days = moment().diff(moment().startOf('year'), 'days') + 1;
const defaultRange = [
moment().subtract(7, 'days').add(1, 'hour').set({ minute: 0, second: 0 }),
moment().add(1, 'hour').set({ minute: 0, second: 0 }),
];
const [dates, setDates] = useState(defaultRange);
const [viewMode, setViewMode] = useState('hour');
const [historyTableList, setHistoryTableList] = useState([]);
const [historyChartList, setHistoryChartList] = useState({});
const [historyRainDetail, sethistoryRainDetail] = useState({});
const [rainDetail, setRainDetail] = useState({});
const columns = [
{ title: '时间', dataIndex: 'time', key: 'time', align: 'center', width: 200 },
{
title: viewMode === 'hour' ? '小时雨量(mm)' : '日雨量(mm)',
dataIndex: 'sumDrp',
key: 'sumDrp',
align: 'center',
render: (rec) => <span>{rec ?? '-'}</span>,
},
];
const option = useMemo(() => {
return drpOption({ echartData: historyChartList });
}, [historyChartList]);
const bottomStats = [
{ label: '最大1h雨量(mm)', value: historyRainDetail?.h1 ?? '-' },
{ label: '最大3h雨量(mm)', value: historyRainDetail?.h3 ?? '-' },
{ label: '最大6h雨量(mm)', value: historyRainDetail?.h6 ?? '-' },
{ label: '最大12h雨量(mm)', value: historyRainDetail?.h12 ?? '-' },
{ label: '本年降雨天数', value: rainDetail?.yearDrpDay, suffix: true, total: days },
{ label: '今日雨量(mm)', value: rainDetail?.today ?? '-' },
{ label: '昨日雨量(mm)', value: rainDetail?.yesterdayDrp ?? '-' },
{ label: '本月雨量(mm)', value: rainDetail?.monthDrp ?? '-' },
{ label: '本年雨量(mm)', value: rainDetail?.yearDrp ?? '-' },
{ label: '本年最大日雨量(mm)', value: rainDetail?.maxDrp ?? '-', suffix: true, total: rainDetail?.maxDrpTime },
];
const getRainDetail = async (stcd) => {
const result = await httpget(apiurl.sq.qth.rainList.queryStPptnDetails, { stcd });
if (result.code === 200) {
setRainDetail(result.data);
}
};
const getRainHistoryData = async (params) => {
try {
const result = await httppost(apiurl.sq.qth.rainList.tableList, params);
if (result.code === 200) {
setHistoryTableList(result.data);
}
} catch (error) {
console.log(error);
}
};
const getRainHistoryChartData = async (params) => {
try {
const result = await httppost(apiurl.sq.qth.rainList.chartList, params);
if (result.code === 200) {
setHistoryChartList(result.data);
}
} catch (error) {
console.log(error);
}
};
const getDayRainHistoryData = async (params) => {
try {
const result = await httppost(apiurl.sq.qth.rainList.dayTableList, params);
if (result.code === 200) {
setHistoryTableList(result.data);
}
} catch (error) {
console.log(error);
}
};
const getDayRainHistoryChartData = async (params) => {
try {
const result = await httppost(apiurl.sq.qth.rainList.dayChartList, params);
if (result.code === 200) {
setHistoryChartList(result.data);
}
} catch (error) {
console.log(error);
}
};
const getRainHistoryDetail = async (params) => {
try {
const result = await httppost(apiurl.sq.qth.rainList.nearbyHistory, params);
if (result.code === 200) {
sethistoryRainDetail(result.data);
}
} catch (error) {
console.log(error);
}
};
const handleSearch = () => {
if (!stcd) return;
const params = {
startTime: dates
? viewMode === 'hour'
? dates[0]?.format('YYYY-MM-DD HH:mm:00')
: dates[0]?.format('YYYY-MM-DD 00:00:00')
: undefined,
endTime: dates
? viewMode === 'hour'
? dates[1]?.format('YYYY-MM-DD HH:mm:59')
: dates[1]?.format('YYYY-MM-DD 59:59:59')
: undefined,
stcd,
};
if (viewMode === 'hour') {
getRainHistoryData(params);
getRainHistoryChartData(params);
} else {
getDayRainHistoryData(params);
getDayRainHistoryChartData(params);
}
getRainHistoryDetail(params);
};
useEffect(() => {
if (stcd) {
getRainDetail(stcd);
handleSearch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stcd]);
useEffect(() => {
if (stcd) {
handleSearch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewMode]);
return (
<div className="rain-monitor-container">
<div className="main-content">
<div className="right-panel">
<div className="panel-header" style={cleanMode ? { justifyContent: 'flex-start' } : {}}>
{!cleanMode && <div className="query-label"><span className="dot"></span></div>}
<div className="query-controls" style={cleanMode ? { marginLeft: 0 } : {}}>
<RangePicker
showTime={viewMode === 'hour'}
value={dates}
onChange={setDates}
style={{ width: 340 }}
format={viewMode === 'hour' ? 'YYYY-MM-DD HH:mm' : 'YYYY-MM-DD'}
allowClear={false}
dropdownClassName="rain-monitor-date-dropdown"
/>
<Button type="primary" className="ant-btn-ghost-blue" icon={<SearchOutlined />} onClick={handleSearch}>查询</Button>
<div className="time-toggle">
<Button
type={viewMode === 'hour' ? 'primary' : 'default'}
className={viewMode === 'hour' ? 'ant-btn-ghost-blue' : 'btn-transparent'}
onClick={() => setViewMode('hour')}
>
小时
</Button>
<Button
type={viewMode === 'day' ? 'primary' : 'default'}
className={viewMode === 'day' ? 'ant-btn-ghost-blue' : 'btn-transparent'}
onClick={() => setViewMode('day')}
>
</Button>
</div>
</div>
</div>
<div className="table-chart-layout">
<div className="data-table">
<Table
columns={columns}
dataSource={historyTableList}
size="small"
pagination={false}
scroll={{ y: 400 }}
rowKey="time"
/>
</div>
<div className="chart-view">
<div className="chart-container">
<ReactEcharts
option={option}
style={{ height: '100%', width: '100%' }}
notMerge={true}
/>
</div>
</div>
</div>
<div className="bottom-stats-grid">
{bottomStats.slice(0, 5).map((i, idx) => <div className="grid-header" key={`h1-${idx}`}>{i.label}</div>)}
{bottomStats.slice(0, 5).map((i, idx) => (
<div className="grid-value" key={`v1-${idx}`}>
<span className={i.suffix ? 'special-text' : ''}>{i.value}</span>
{i.total && <span>/{i.total}</span>}
</div>
))}
{bottomStats.slice(5, 10).map((i, idx) => <div className="grid-header" key={`h2-${idx}`}>{i.label}</div>)}
{bottomStats.slice(5, 10).map((i, idx) => (
<div className="grid-value" key={`v2-${idx}`}>
{i.value}
{i.total && <span className="special-text">{i.total ? `(${moment(i.total).format('YYYY-MM-DD')})` : ''}</span>}
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,179 @@
import { useMemo } from 'react';
import echarts from 'echarts/lib/echarts';
export default function DrpOption({ echartData, grid }) {
let totalDrp = 0;
const DRPLEVEL = [10, 20, 50, 100, 250];
const maxVal = DRPLEVEL.find(o => o > totalDrp);
const xMaxVal = echartData?.actual ? DRPLEVEL.find(o => {
let max = Math.max(...echartData?.actual || [])
return o > max
}):maxVal
const yMaxVal = echartData?.actual ? DRPLEVEL.find(o => {
let max = Math.max(...echartData?.total)
return o > max
}): maxVal
return {
tooltip: {
trigger: 'axis',
},
grid: grid || {
x: 40,
y: 30,
x2: 30,
y2: 28,
borderWidth: 0
},
legend: {
// 显示图例
show: true,
// 图例的位置
data: ['实测', '累计'],
textStyle: { color: '#fff' },
},
calculable: true,
xAxis: [
{
type: 'category',
data: echartData?.time,
splitLine: {
show: false
},
axisLabel: {
color: '#fff',
fontSize: 12,
formatter: val => val.substr('2020-'.length, 11)
},
axisLine: {
lineStyle: {
color: '#07a6ff',
width: 0.5,
}
},
axisTick: {
show: false,
},
}
],
yAxis: [
{
type: 'value',
position: 'left',
name: "雨量mm",
nameTextStyle: { color: '#fff' },
splitLine: {
show: true,
lineStyle: {
color: '#07a6ff',
width: 0.25,
type: 'dashed'
}
},
axisLabel: {
color: '#fff',
fontSize: 12,
},
axisLine: {
show: false
},
axisTick: {
show: false,
},
min: 0,
max: xMaxVal
},
{
type: 'value',
position: 'right',
nameTextStyle: { color: '#fff' },
name:"累计mm",
splitLine: {
show: true,
lineStyle: {
color: '#07a6ff',
width: 0.25,
type: 'dashed'
}
},
axisLabel: {
color: '#fff',
fontSize: 12,
},
axisLine: {
show: false
},
axisTick: {
show: false,
},
min: 0,
max: yMaxVal
}
],
series: [
{
name: '实测',
type: 'bar',
barWidth: '60%',
data: echartData?.actual,
itemStyle: {
normal: {
barBorderRadius: [3, 3, 0, 0],
color: new echarts.graphic.LinearGradient(
0, 0, 0, 1,
[
{ offset: 0, color: '#3876cd' },
{ offset: 0.5, color: '#45b4e7' },
{ offset: 1, color: '#54ffff' }
]
),
},
},
label: {
show: false,
},
markPoint: {
data: [
{ type: 'max', name: '最大值', symbol: 'circle', symbolSize: 1, symbolOffset: [0, -12] },
]
},
},
{
yAxisIndex: 1,
name: '累计',
type: 'line',
showSymbol: false,
label: {
show: false,
},
data: echartData?.total,
lineStyle: {
normal: {
width: 1,
}
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(3, 194, 236, 0.3)'
}, {
offset: 0.8,
color: 'rgba(3, 194, 236, 0)'
}
], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: '#03C2EC'
}
},
}
]
};
}

View File

@ -0,0 +1,98 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Select } from 'antd';
import usePageTable from '@/components/crud/usePageTable';
import { createCrudService } from '@/components/crud/_';
import apiurl from '@/service/apiurl';
import { httpget,httppost } from '@/utils/request';
import moment from 'moment';
import { rainlist } from '@/service/station';
import RightPanel from './RightPanel';
import './index.less';
const RainMonitor = () => {
//实时雨量
const [selectList, setSelectList] = useState([])
const [selected, setSelected] = useState('')
const [rainDetail, setRainDetail] = useState({})
const stats = [
{ label: '近1小时', value: rainDetail?.h1 ?? '-', unit: 'mm' },
{ label: '近3小时', value: rainDetail?.h3 ?? '-', unit: 'mm' },
{ label: '近6小时', value: rainDetail?.h6 ?? '-', unit: 'mm' },
{ label: '近12小时', value: rainDetail?.h12 ?? '-', unit: 'mm' },
{ label: '近24小时', value: rainDetail?.h24 ?? '-', unit: 'mm' },
{ label: '近48小时', value: rainDetail?.h48 ?? '-', unit: 'mm' },
];
// 获取雨情站点
const getRainStationList = async() => {
try {
const data = await rainlist({})
setSelectList(data.map(item => ({ label: item.stnm, value: item.stcd,...item})))
setSelected(data[0].stcd)
} catch (error) {
console.log(error);
}
}
// 获取实时详细雨量的 近几小时数据
const getRainDetail = async (stcd) => {
const result = await httpget(apiurl.sq.qth.rainList.queryStPptnDetails, { stcd });
if (result.code == 200) {
setRainDetail(result.data)
}
}
useEffect(() => {
getRainStationList();
}, []);
useEffect(() => {
if (selected) {
getRainDetail(selected);
}
}, [selected]);
return (
<div className="rain-monitor-container">
<div className="main-content">
{/* Left Side: Stats Cards */}
<div className="left-panel">
<div className="panel-header">
<div className="query-label"><span className="dot"></span></div>
<div className="station-select">
<span>站点</span>
<Select
value={selected}
onChange={setSelected}
style={{ width: 200 }}
options={selectList}
/>
</div>
</div>
<div className="update-time-banner">
雨情最新上报时间{rainDetail.tm}
</div>
<div className="stats-grid">
{stats.map((item, idx) => (
<div key={idx} className="stat-card">
<div className="stat-value">
<span className="num">{item.value}</span>
<span className="unit"> {item.unit}</span>
</div>
<div className="stat-label">{item.label}</div>
</div>
))}
</div>
</div>
<RightPanel stcd={selected} />
</div>
</div>
);
};
export default RainMonitor;

View File

@ -0,0 +1,324 @@
.rain-monitor-container {
height: 100%;
display: flex;
flex-direction: column;
color: #fff;
.main-content {
flex: 1;
display: flex;
gap: 16px;
overflow: hidden;
.left-panel {
// flex: 1; /* 1:2 ratio */
display: flex;
flex-direction: column;
gap: 15px;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 42px;
.query-label {
display: flex;
align-items: center;
font-size: 16px;
font-weight: bold;
.dot {
width: 4px;
height: 16px;
background: #00a0e9;
margin-right: 8px;
}
}
.station-select {
display: flex;
align-items: center;
span { margin-right: 8px; }
}
}
.update-time-banner {
background: rgba(0, 160, 233, 0.1);
border: 1px solid rgba(0, 160, 233, 0.3);
padding: 8px;
text-align: center;
border-radius: 4px;
color: #fff;
width: 70%;
margin: 0 auto;
}
.stats-grid {
display: flex;
flex-wrap: wrap;
gap: 42px 20px;
.stat-card {
width: calc(50% - 10px);
background: rgba(255, 255, 255, 0.05);
padding:25px 12px;
border-radius: 4px;
text-align: center;
border: 1px solid transparent;
transition: all 0.3s;
&:hover {
border-color: rgba(0, 160, 233, 0.5);
background: rgba(0, 160, 233, 0.1);
}
.stat-value {
margin-bottom: 24px;
.num {
font-size: 20px;
font-weight: bold;
color: #00e5ff;
}
.unit {
font-size: 16px;
color: rgba(255, 255, 255, 0.6);
}
}
.stat-label {
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
}
}
}
.right-panel {
flex: 2; /* 1:2 ratio */
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 42px;
.query-label {
display: flex;
align-items: center;
font-size: 16px;
font-weight: bold;
.dot {
width: 4px;
height: 16px;
background: #00a0e9;
margin-right: 8px;
}
}
.query-controls {
display: flex;
gap: 12px;
align-items: center;
.time-toggle {
display: flex;
gap: 8px;
margin-left: 16px;
.btn-transparent {
background: transparent;
border: 1px solid #00a0e9;
color: #fff;
box-shadow: none;
&:hover {
color: #00a0e9;
background: rgba(0, 160, 233, 0.1);
}
}
}
}
}
.table-chart-layout {
flex: 1;
display: flex;
gap: 16px;
overflow: hidden;
.data-table {
width: 330px;
border-radius: 4px;
}
.chart-view {
flex: 1;
display: flex;
flex-direction: column;
border-radius: 4px;
padding: 12px;
position: relative;
.chart-container {
flex: 1;
min-height: 0;
}
}
}
.bottom-stats-grid {
display: flex;
flex-wrap: wrap;
border-top: 1px solid rgba(0, 160, 233, 0.2);
border-left: 1px solid rgba(0, 160, 233, 0.2);
.grid-header {
width: 20%;
background: rgba(0, 160, 233, 0.1);
color: rgba(255, 255, 255);
padding: 8px 4px;
text-align: center;
font-size: 14px;
border-right: 1px solid rgba(0, 160, 233, 0.2);
border-bottom: 1px solid rgba(0, 160, 233, 0.2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.grid-value {
width: 20%;
color: #fff;
padding: 8px 4px;
text-align: center;
font-size: 14px;
font-weight: bold;
border-right: 1px solid rgba(0, 160, 233, 0.2);
border-bottom: 1px solid rgba(0, 160, 233, 0.2);
.special-text {
color: #00e5ff;
font-weight: normal;
}
}
}
}
}
/* Time Picker Panel Styles - REMOVED (moved to .rain-monitor-date-dropdown) */
}
/* Dropdown Styles - Must be outside .rain-monitor-container because it renders in body */
.rain-monitor-date-dropdown {
background-color: #082f4d; // Ensure main background is dark
.ant-picker-panel-container {
background-color: #082f4d;
box-shadow: 0 0 10px rgba(0, 160, 233, 0.3);
border: 1px solid rgba(0, 160, 233, 0.3);
}
.ant-picker-header {
color: #fff;
border-bottom: 1px solid rgba(0, 160, 233, 0.2);
button {
color: #fff;
&:hover {
color: #00a0e9;
}
}
}
.ant-picker-content {
th {
color: #00a0e9;
}
}
.ant-picker-cell {
color: rgba(255, 255, 255, 0.7);
&.ant-picker-cell-in-view {
color: #fff;
}
&:hover .ant-picker-cell-inner {
background: rgba(0, 160, 233, 0.2);
}
&.ant-picker-cell-selected .ant-picker-cell-inner {
background: #00a0e9;
color: #fff;
}
// Range Selection Fix - Remove "Red Box" Color (Blue Background)
&-in-range::before {
background: transparent !important; // Remove the blue background
}
// Ensure range start/end still have background
&-range-start .ant-picker-cell-inner,
&-range-end .ant-picker-cell-inner {
background: #00a0e9 !important;
color: #fff;
}
}
/* Time Picker Styles */
.ant-picker-time-panel {
border-left: 1px solid rgba(0, 160, 233, 0.2);
}
.ant-picker-datetime-panel {
.ant-picker-time-panel {
border-left: 1px solid rgba(0, 160, 233, 0.2);
}
}
.ant-picker-time-panel-column {
border-left: 1px solid rgba(0, 160, 233, 0.2);
&::after {
height: auto;
}
> li.ant-picker-time-panel-cell {
.ant-picker-time-panel-cell-inner {
color: rgba(255, 255, 255, 0.8) !important; // Force white color
&:hover {
background: rgba(0, 160, 233, 0.2);
color: #fff !important;
}
}
&.ant-picker-time-panel-cell-selected {
.ant-picker-time-panel-cell-inner {
background: rgba(0, 160, 233, 0.4);
color: #00a0e9 !important;
font-weight: bold;
}
}
}
}
.ant-picker-footer {
border-top: 1px solid rgba(0, 160, 233, 0.2);
.ant-picker-today-btn {
color: #00a0e9;
}
.ant-picker-ok {
.ant-btn {
background-color: #00a0e9;
border-color: #00a0e9;
&:hover {
background-color: #008cc9;
}
}
}
}
}

View File

@ -5,42 +5,10 @@ import usePageTable from '@/components/crud/usePageTable';
import { createCrudService } from '@/components/crud/_';
import apiurl from '@/service/apiurl';
import './index.less';
import RainMonitor from './RainMonitor';
const { RangePicker } = DatePicker;
const RainPanel = () => {
const { tableProps, search } = usePageTable(createCrudService(apiurl.sq.qth.rainList).find);
const [range, setRange] = useState();
useEffect(() => {
search({ search: {} });
}, []);
const columns = [
{ title: '时间', dataIndex: 'tm', key: 'tm', width: 140, align: 'center' },
{ title: '小时雨量(mm)', dataIndex: 'drp', key: 'drp', align: 'center' },
];
const option = useMemo(() => ({
title: { text: '雨量趋势', left: 'center', textStyle: { color: '#fff' } },
tooltip: { trigger: 'axis' },
grid: { left: '8%', right: '4%', bottom: '10%', top: '15%' },
xAxis: { type: 'category', data: (tableProps.dataSource || []).map(i => i.tm), axisLabel: { color: '#fff' } },
yAxis: { type: 'value', axisLabel: { color: '#fff' }, splitLine: { show: true, lineStyle: { color: 'rgba(255,255,255,0.2)' } } },
series: [{ type: 'line', smooth: true, data: (tableProps.dataSource || []).map(i => i.drp), areaStyle: { color: 'rgba(0,160,233,0.25)' }, lineStyle: { color: '#00a0e9' } }],
}), [tableProps.dataSource]);
return (
<div className="awm-grid">
<div className="awm-left">
<div className="awm-toolbar">
<RangePicker value={range} onChange={setRange} />
<Button type="primary" className="ant-btn-ghost-blue" onClick={() => search({ search: { range } })}>查询</Button>
</div>
<Table columns={columns} {...tableProps} size="small" pagination={false} scroll={{ y: 360 }} />
</div>
<div className="awm-right">
<ReactEcharts option={option} style={{ height: '100%', width: '100%' }} />
</div>
</div>
);
};
const ReservoirPanel = () => {
const columns = [
@ -107,7 +75,7 @@ const SafetyPanel = () => {
};
const AllWeatherModal = ({ active }) => {
if (active === 'rain') return <RainPanel />;
if (active === 'rain') return <RainMonitor />;
if (active === 'reservoir') return <ReservoirPanel />;
if (active === 'flow') return <FlowPanel />;
return <SafetyPanel />;

View File

@ -1,7 +1,9 @@
import React from "react";
import CommonModal from '../CommonModal';
import { config } from "@/config";
const PdfView = ({ visible, title, onClose, url, fileId }) => {
console.log(url);
return (
<CommonModal
visible={visible}