Compare commits

...

2 Commits

Author SHA1 Message Date
李神峰 39c270984a Merge branch 'lsf-dev' 2026-02-27 10:33:41 +08:00
李神峰 db011df5a6 feat():安全监测模块开发 2026-02-26 17:56:12 +08:00
7 changed files with 1244 additions and 6 deletions

View File

@ -123,6 +123,13 @@
}
}
.ant-table-cell-fix-left, .ant-table-cell-fix-right{
background: transparent !important;
}
.ant-table-thead > tr > th, .ant-table-tbody > tr > td, .ant-table tfoot > tr > th, .ant-table tfoot > tr > td{
text-align: center;
}
// Table Scrollbar Fix (Remove white strip)
.ant-table-body, .ant-table-content {
&::-webkit-scrollbar {

View File

@ -0,0 +1,304 @@
import React, { useState, useEffect } from 'react';
import { Row, Col, Table, Radio } from 'antd';
import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts';
// import './ProcessLineChart.less'; // Reuse styles or create new ones
// Mock Data
const mockData = {
dates: ['2026-01-26', '2026-01-27', '2026-01-28', '2026-01-29', '2026-01-30', '2026-01-31', '2026-02-01', '2026-02-02', '2026-02-03', '2026-02-04', '2026-02-05', '2026-02-06', '2026-02-07'],
reservoirLevel: [704.5, 704.4, 704.6, 704.5, 704.4, 704.5, 704.6, 704.5, 704.39, 704.4, 704.5, 704.6, 704.5],
points: {
'01': {
x: [-89.88, -89.48, -88.77, -89.0, -88.19, -88.5, -88.44, -89.1, -88.75, -89.2, -88.9, -89.0, -88.8],
y: [-26.96, -27.22, -28.33, -28.17, -28.48, -29.06, -28.73, -27.9, -28.5, -28.2, -28.1, -28.0, -28.4],
z: [10.5, 10.6, 10.4, 10.5, 10.7, 10.6, 10.5, 10.6, 10.5, 10.4, 10.5, 10.6, 10.5]
},
'02': {
x: [-114.67, -114.43, -113.94, -113.45, -113.72, -113.51, -113.9, -114.2, -114.29, -113.8, -114.0, -114.1, -114.3],
y: [-24.63, -25.46, -24.91, -25.42, -24.57, -25.23, -25.19, -24.8, -25.0, -25.3, -25.1, -25.2, -25.4],
z: [15.2, 15.3, 15.1, 15.2, 15.4, 15.3, 15.2, 15.3, 15.2, 15.1, 15.2, 15.3, 15.2]
}
}
};
const getChartOption = (data, mainType, subType) => {
const pointNames = Object.keys(data.points);
const colors = ['#5470C6', '#91CC75', '#EE6666'];
// Determine Y-axis name and data key based on types
let yAxisName = '';
let dataKey = '';
if (mainType === 'horizontal') {
if (subType === 'upDown') {
yAxisName = '上下游水平位移(mm)';
dataKey = 'x';
} else {
yAxisName = '左右岸水平位移(mm)';
dataKey = 'y';
}
} else {
yAxisName = '垂直位移(mm)';
dataKey = 'z';
}
return {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', crossStyle: { color: '#999' } }
},
legend: {
data: ['库水位(m)', ...pointNames],
textStyle: { color: '#fff' }
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: data.dates,
axisPointer: { type: 'shadow' },
axisLine: { lineStyle: { color: '#86909C' } },
axisLabel: { color: '#fff' }
}
],
yAxis: [
{
type: 'value',
name: yAxisName,
axisLabel: { formatter: '{value}', color: '#fff' },
nameTextStyle: { color: '#fff' },
splitLine: { lineStyle: { color: '#4E5969', type: 'dashed' } }
},
{
type: 'value',
name: '库水位(m)',
min: 690,
max: 715,
interval: 5,
axisLabel: { formatter: '{value}', color: '#fff' },
nameTextStyle: { color: '#fff' },
splitLine: { show: false }
}
],
series: [
{
name: '库水位(m)',
type: 'line',
yAxisIndex: 1,
data: data.reservoirLevel,
lineStyle: { color: '#EE6666', width: 2 },
itemStyle: { color: '#EE6666' },
symbol: 'circle',
symbolSize: 6
},
...pointNames.map((name, index) => ({
name: name,
type: 'line',
yAxisIndex: 0,
data: data.points[name][dataKey],
lineStyle: { color: colors[index] },
itemStyle: { color: colors[index] }
}))
],
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{
start: 0, end: 100,
handleIcon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z',
handleSize: '80%',
handleStyle: { color: '#fff', shadowBlur: 3, shadowColor: 'rgba(0, 0, 0, 0.6)', shadowOffsetX: 2, shadowOffsetY: 2 },
textStyle: { color: '#fff' }
}
]
};
};
const DeformationPanel = () => {
const [chartData] = useState(mockData);
const [mainType, setMainType] = useState('horizontal'); // 'horizontal' | 'vertical'
const [subType, setSubType] = useState('upDown'); // 'upDown' | 'leftRight'
// Generate Table Columns
const getTableColumns = () => {
const pointNames = Object.keys(chartData.points);
// Fixed columns
const columns = [
{
title: '监测日期',
dataIndex: 'date',
key: 'date',
width: 120,
fixed: 'left',
}
];
// Dynamic columns for each point
if (mainType === 'horizontal') {
// For horizontal, show x and y
columns.push({
title: '水平位移(mm)',
children: pointNames.map(name => ({
title: name,
children: [
{ title: 'x', dataIndex: `${name}_x`, key: `${name}_x`, width: 80 },
{ title: 'y', dataIndex: `${name}_y`, key: `${name}_y`, width: 80 }
]
}))
});
} else {
// For vertical, show z
columns.push({
title: '垂直位移(mm)',
children: pointNames.map(name => ({
title: name,
dataIndex: `${name}_z`,
key: `${name}_z`,
width: 100
}))
});
}
return columns;
};
// Generate Table Data
const getTableData = () => {
const dataSource = chartData.dates.map((date, index) => {
const row = { key: index, date: date };
Object.keys(chartData.points).forEach(name => {
if (mainType === 'horizontal') {
row[`${name}_x`] = chartData.points[name].x[index];
row[`${name}_y`] = chartData.points[name].y[index];
} else {
row[`${name}_z`] = chartData.points[name].z[index];
}
});
return row;
});
// Summary (Max, Min, Range)
const summary = {
max: { date: '最大值', key: 'max' },
min: { date: '最小值', key: 'min' },
range: { date: '变幅', key: 'range' },
};
const pointNames = Object.keys(chartData.points);
const fields = [];
pointNames.forEach(name => {
if (mainType === 'horizontal') {
fields.push(`${name}_x`, `${name}_y`);
} else {
fields.push(`${name}_z`);
}
});
fields.forEach(field => {
const [name, key] = field.split('_');
const values = chartData.points[name][key];
const validValues = values.filter(v => v !== null && v !== undefined);
if (validValues.length > 0) {
const max = Math.max(...validValues);
const min = Math.min(...validValues);
summary.max[field] = max.toFixed(2);
summary.min[field] = min.toFixed(2);
summary.range[field] = (max - min).toFixed(2);
} else {
summary.max[field] = '-';
summary.min[field] = '-';
summary.range[field] = '-';
}
});
return [...dataSource, summary.max, summary.min, summary.range];
};
return (
<Row gutter={16} style={{ height: '100%' }}>
{/* Left Chart */}
<Col span={14} style={{ height: '100%' }}>
<ReactECharts
option={getChartOption(chartData, mainType, subType)}
style={{ height: '100%', width: '100%' }}
/>
</Col>
{/* Right Table */}
<Col span={10} style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ marginBottom: 10, display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
{mainType === 'horizontal' && (
<Radio.Group
value={subType}
onChange={e => setSubType(e.target.value)}
buttonStyle="solid"
size="small"
style={{ marginRight: 12 }}
>
<Radio.Button value="upDown" style={{
borderRadius: '4px 0 0 4px',
marginLeft: '-1px',
background: subType === 'upDown' ? 'rgba(0, 160, 233, 0.8)' : 'rgba(18, 56, 102, 0.6)',
borderColor: '#00a0e9',
color: '#fff'
}}>上下游</Radio.Button>
<Radio.Button value="leftRight" style={{
borderRadius: '0 4px 4px 0',
marginLeft: '-1px',
background: subType === 'leftRight' ? 'rgba(0, 160, 233, 0.8)' : 'rgba(18, 56, 102, 0.6)',
borderColor: '#00a0e9',
color: '#fff'
}}>左右岸</Radio.Button>
</Radio.Group>
)}
<Radio.Group
value={mainType}
onChange={e => {
setMainType(e.target.value);
if (e.target.value === 'horizontal') {
setSubType('upDown'); // Reset subType when switching back
}
}}
buttonStyle="solid"
size="small"
>
<Radio.Button value="horizontal" style={{
borderRadius: '4px 0 0 4px',
background: mainType === 'horizontal' ? 'rgba(0, 160, 233, 0.8)' : 'rgba(18, 56, 102, 0.6)',
borderColor: '#00a0e9',
color: '#fff'
}}>水平位移</Radio.Button>
<Radio.Button value="vertical" style={{
borderRadius: '0 4px 4px 0',
marginLeft: '-1px',
background: mainType === 'vertical' ? 'rgba(0, 160, 233, 0.8)' : 'rgba(18, 56, 102, 0.6)',
borderColor: '#00a0e9',
color: '#fff'
}}>垂直位移</Radio.Button>
</Radio.Group>
</div>
<Table
columns={getTableColumns()}
dataSource={getTableData()}
pagination={false}
scroll={{ y: 'calc(100vh - 350px)' }}
size="small"
/>
</Col>
</Row>
);
};
export default DeformationPanel;

View File

@ -0,0 +1,308 @@
import React, { useState, useEffect } from 'react';
import { Row, Col, Table } from 'antd';
import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts';
// import './ProcessLineChart.less';
// Mock Data based on the screenshot
const mockData = {
dates: ['2026-01-26', '2026-01-27', '2026-01-28', '2026-01-29', '2026-01-30', '2026-01-31', '2026-02-01', '2026-02-02', '2026-02-03', '2026-02-04', '2026-02-05', '2026-02-06', '2026-02-07'],
rainfall: [0, 0, 0.5, 0.5, 0, 0, 0, 0, 3, 0, 0, 0, 0],
reservoirLevel: [null, null, null, null, 1141.7, 1141.69, 1141.65, 1141.52, 1144.41, 1144.34, 1144.34, 1144.33, 1144.3],
pipes: {
'UPD1': [1142, 1141.96, 1141.8, 1141.71, 1141.7, 1141.69, 1141.65, 1141.52, 1141.39, 1141.42, 1141.64, 1141.55, 1141.73],
'UPD2': [1132.42, 1132.43, 1132.43, 1132.4, 1132.42, 1132.43, 1132.43, 1132.43, 1132.43, 1132.41, 1132.4, 1132.4, 1132.41],
'UPD3': [1125.47, 1125.47, 1125.46, 1125.41, 1125.48, 1125.51, 1125.51, 1125.52, 1125.48, 1125.42, 1125.41, 1125.46, 1125.46],
}
};
const getChartOption = (data) => {
const pipeNames = Object.keys(data.pipes);
const colors = ['#5470C6', '#91CC75', '#EE6666', '#73C0DE', '#3BA272', '#FC8452', '#9A60B4', '#EA7CCC'];
return {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999'
}
}
},
legend: {
data: ['雨量', '库水位', ...pipeNames],
textStyle: {
color: '#fff'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: data.dates,
axisPointer: {
type: 'shadow'
},
axisLine: {
lineStyle: {
color: '#86909C'
}
},
axisLabel: {
color: '#fff'
}
}
],
yAxis: [
{
type: 'value',
name: '水位(m)',
min: 1115,
max: 1155,
interval: 5,
axisLabel: {
formatter: '{value}',
color: '#fff'
},
nameTextStyle: {
color: '#fff'
},
splitLine: {
lineStyle: {
color: '#4E5969'
}
}
},
{
type: 'value',
name: '雨量(mm)',
min: 0,
max: 500,
interval: 100,
axisLabel: {
formatter: '{value}',
color: '#fff'
},
nameTextStyle: {
color: '#fff'
},
splitLine: {
show: false
}
}
],
series: [
{
name: '雨量',
type: 'bar',
yAxisIndex: 1,
tooltip: {
valueFormatter: function (value) {
return value + ' mm';
}
},
data: data.rainfall,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
}
},
{
name: '库水位',
type: 'line',
yAxisIndex: 0,
tooltip: {
valueFormatter: function (value) {
return value + ' m';
}
},
data: data.reservoirLevel,
lineStyle: {
color: colors[2]
}
},
...pipeNames.map((name, index) => ({
name: name,
type: 'line',
yAxisIndex: 0,
tooltip: {
valueFormatter: function (value) {
return value + ' m';
}
},
data: data.pipes[name],
lineStyle: {
color: colors[index + 3]
}
}))
],
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
},
{
start: 0,
end: 100,
handleIcon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z',
handleSize: '80%',
handleStyle: {
color: '#fff',
shadowBlur: 3,
shadowColor: 'rgba(0, 0, 0, 0.6)',
shadowOffsetX: 2,
shadowOffsetY: 2
},
textStyle: {
color: '#fff'
}
}
]
};
};
const ProcessLineChart = () => {
const [chartData] = useState(mockData);
const tableData = chartData.dates.map((date, index) => {
const row = {
key: index,
date: date,
reservoirLevel: chartData.reservoirLevel[index],
rainfall: chartData.rainfall[index],
};
for (const pipeName in chartData.pipes) {
row[pipeName] = chartData.pipes[pipeName][index];
}
return row;
});
const pipeNames = Object.keys(chartData.pipes);
const columns = [
{ title: '监测日期', dataIndex: 'date', key: 'date', width: 120, fixed: 'left', align: 'center' },
{ title: '库水位(m)', dataIndex: 'reservoirLevel', key: 'reservoirLevel', width: 100, align: 'center' },
{ title: '雨量(mm)', dataIndex: 'rainfall', key: 'rainfall', width: 100, align: 'center' },
{
title: '渗压水位(m)',
align: 'center',
children: pipeNames.map(name => ({
title: name,
dataIndex: name,
key: name,
width: 100,
align: 'center'
})),
},
];
const summaryNode = () => {
const summary = {
max: { date: '最大值' },
min: { date: '最小值' },
range: { date: '变幅' },
};
const fields = ['reservoirLevel', 'rainfall', ...pipeNames];
fields.forEach(field => {
const values = chartData[field] || chartData.pipes[field];
let max = -Infinity, min = Infinity, maxDate = '', minDate = '';
values.forEach((v, i) => {
if (v !== null && v !== undefined) {
if (v > max) { max = v; maxDate = chartData.dates[i]; }
if (v < min) { min = v; minDate = chartData.dates[i]; }
}
});
if (max === -Infinity) {
summary.max[field] = 'N/A';
summary.min[field] = 'N/A';
summary.range[field] = 'N/A';
summary.max[`${field}_date`] = '';
summary.min[`${field}_date`] = '';
} else {
summary.max[field] = max.toFixed(2);
summary.min[field] = min.toFixed(2);
summary.range[field] = (max - min).toFixed(2);
summary.max[`${field}_date`] = maxDate;
summary.min[`${field}_date`] = minDate;
}
});
const cellContentStyle = { whiteSpace: 'pre-line', textAlign: 'center', verticalAlign: 'middle', borderBottom: '1px solid rgba(0, 160, 233, 0.3)' };
const rowStyle = { background: 'rgba(0, 33, 64, 0.85)', color: '#fff' };
const fixedCellStyle = {
...cellContentStyle,
background: 'rgba(0, 33, 64, 0.95)',
color: '#fff',
fontWeight: 'bold',
boxShadow: '2px 0 5px rgba(0,0,0,0.3)'
};
return (
<Table.Summary fixed>
<Table.Summary.Row style={rowStyle}>
<Table.Summary.Cell index={0} fixed="left" style={fixedCellStyle}>{summary.max.date}</Table.Summary.Cell>
<Table.Summary.Cell index={1} style={cellContentStyle}>{summary.max.reservoirLevel}</Table.Summary.Cell>
<Table.Summary.Cell index={2} style={cellContentStyle}>{summary.max.rainfall}</Table.Summary.Cell>
{pipeNames.map((name, i) => <Table.Summary.Cell key={`${name}-max`} index={i + 3} style={cellContentStyle}>{summary.max[name]}</Table.Summary.Cell>)}
</Table.Summary.Row>
<Table.Summary.Row style={rowStyle}>
<Table.Summary.Cell index={0} fixed="left" style={fixedCellStyle}>日期</Table.Summary.Cell>
<Table.Summary.Cell index={1} style={cellContentStyle}>{summary.max.reservoirLevel_date}</Table.Summary.Cell>
<Table.Summary.Cell index={2} style={cellContentStyle}>{summary.max.rainfall_date}</Table.Summary.Cell>
{pipeNames.map((name, i) => <Table.Summary.Cell key={`${name}-max-date`} index={i + 3} style={cellContentStyle}>{summary.max[`${name}_date`]}</Table.Summary.Cell>)}
</Table.Summary.Row>
<Table.Summary.Row style={rowStyle}>
<Table.Summary.Cell index={0} fixed="left" style={fixedCellStyle}>{summary.min.date}</Table.Summary.Cell>
<Table.Summary.Cell index={1} style={cellContentStyle}>{summary.min.reservoirLevel}</Table.Summary.Cell>
<Table.Summary.Cell index={2} style={cellContentStyle}>{summary.min.rainfall}</Table.Summary.Cell>
{pipeNames.map((name, i) => <Table.Summary.Cell key={`${name}-min`} index={i + 3} style={cellContentStyle}>{summary.min[name]}</Table.Summary.Cell>)}
</Table.Summary.Row>
<Table.Summary.Row style={rowStyle}>
<Table.Summary.Cell index={0} fixed="left" style={fixedCellStyle}>日期</Table.Summary.Cell>
<Table.Summary.Cell index={1} style={cellContentStyle}>{summary.min.reservoirLevel_date}</Table.Summary.Cell>
<Table.Summary.Cell index={2} style={cellContentStyle}>{summary.min.rainfall_date}</Table.Summary.Cell>
{pipeNames.map((name, i) => <Table.Summary.Cell key={`${name}-min-date`} index={i + 3} style={cellContentStyle}>{summary.min[`${name}_date`]}</Table.Summary.Cell>)}
</Table.Summary.Row>
<Table.Summary.Row style={rowStyle}>
<Table.Summary.Cell index={0} fixed="left" style={fixedCellStyle}>{summary.range.date}</Table.Summary.Cell>
<Table.Summary.Cell index={1} style={cellContentStyle}>{summary.range.reservoirLevel}</Table.Summary.Cell>
<Table.Summary.Cell index={2} style={cellContentStyle}>{summary.range.rainfall}</Table.Summary.Cell>
{pipeNames.map((name, i) => <Table.Summary.Cell key={`${name}-range`} index={i + 3} style={cellContentStyle}>{summary.range[name]}</Table.Summary.Cell>)}
</Table.Summary.Row>
</Table.Summary>
);
};
return (
<Row gutter={16} style={{ height: '100%' }}>
<Col span={12} style={{ height: '98%' }}>
<ReactECharts
option={getChartOption(chartData)}
style={{ height: '100%', width: '100%' }}
/>
</Col>
<Col span={12} style={{ height: '100%' }}>
<Table
columns={columns}
dataSource={tableData}
pagination={false}
scroll={{ y: 'calc(100vh - 650px)' }}
summary={summaryNode}// Adjust based on your layout
/>
</Col>
</Row>
);
};
export default ProcessLineChart;

View File

@ -0,0 +1,401 @@
import * as echarts from 'echarts';
export const getOption = (data = {}) => {
const {
waterLevel = 113.8,
floodLevel = 128.42,
pipes = [
{ name: 'UPD4', x: 110, top: 175, bottom: 60, level: 103.67 },
{ name: 'UPD1', x: 160, top: 175, bottom: 60, level: 103.67 },
{ name: 'UPD2', x: 190, top: 150, bottom: 60, level: 102.89 },
{ name: 'UPD3', x: 210, top: 130, bottom: 60, level: 101.12 },
]
} = data;
// Dam Geometry Configuration
const foundationY = 40;
const crestY = 180;
// Left Slope (Upstream)
const leftToe = [20, foundationY];
const leftCrest = [130, crestY];
// Right Slope (Downstream) with Berm
const rightCrest = [150, crestY];
const bermElevation = 120;
const bermStart = [190, bermElevation];
const bermEnd = [210, bermElevation];
const rightToe = [250, foundationY];
const distanceFromCrest = 20; // Distance from top of core to dam crest
const coreBaseLeft = 125;
const coreBaseRight = 155;
const coreTopLeft = 135;
const coreTopRight = 145;
const coreBottomY = foundationY;
// const coreTopY = crestY - distanceFromCrest;
const coreTopY = floodLevel;
// Dam Toe Weight (Gray Mound)
// Shifted left to intersect with the main slope
const toeWeightPoints = [
[230, foundationY],
[240, 55],
[245, 55],
[255, foundationY]
];
// Colors
const damColor = '#Decbb6'; // Light beige for shell
const coreColor = '#D2a88d'; // Darker for core
const foundationColor = '#8B4513'; // Dark brown
const waterColor = 'rgba(0, 150, 136, 0.8)';
const saturationLineColor = '#00a0e9'; // Bright blue
const floodLineColor = 'red';
const toeColor = '#808080'; // Gray for toe
// Calculations
// Upstream Slope Line: y - y1 = m(x - x1)
const mUp = (leftCrest[1] - leftToe[1]) / (leftCrest[0] - leftToe[0]);
const cUp = leftToe[1] - mUp * leftToe[0];
// Intersection of Water Level with Upstream Slope
// x = (y - c) / m
const waterX = (waterLevel - cUp) / mUp;
const floodX = (floodLevel - cUp) / mUp;
// Render Pipe Function
const renderPipe = (params, api) => {
const x = api.value(0);
const top = api.value(1);
const bottom = api.value(2);
const level = api.value(3);
const name = api.value(4);
const topPos = api.coord([x, top]);
const bottomPos = api.coord([x, bottom]);
const levelPos = api.coord([x, level]);
const width = 12;
return {
type: 'group',
children: [
{
type: 'rect',
shape: {
x: topPos[0] - width / 2,
y: topPos[1],
width: width,
height: levelPos[1] - topPos[1]
},
style: {
fill: 'rgba(200,200,200,0.2)',
stroke: '#999',
lineWidth: 1
}
},
{
type: 'rect',
shape: {
x: levelPos[0] - width / 2,
y: levelPos[1],
width: width,
height: bottomPos[1] - levelPos[1]
},
style: {
fill: '#00a0e9',
stroke: '#999',
lineWidth: 1
}
},
{
type: 'text',
style: {
text: name,
x: topPos[0],
y: topPos[1] - 15,
textAlign: 'center',
fill: '#fff',
fontSize: 12
}
},
{
type: 'text',
style: {
text: level.toFixed(2) + 'm',
x: levelPos[0] + width / 2 + 5,
y: levelPos[1],
textAlign: 'left',
textVerticalAlign: 'middle',
fill: '#fff',
fontSize: 11
}
}
]
};
};
// Saturation Line Logic
const damCenterX = 140;
const leftPipes = pipes.filter(p => p.x < damCenterX).sort((a, b) => a.x - b.x);
const rightPipes = pipes.filter(p => p.x >= damCenterX).sort((a, b) => a.x - b.x);
const saturationPoints = [];
// 1. Start at water surface
saturationPoints.push([waterX, waterLevel]);
// 2. Handle Left Side
if (leftPipes.length > 0) {
// Connect all left pipes
leftPipes.forEach(p => saturationPoints.push([p.x, p.level]));
// Extend to center from the last left pipe
saturationPoints.push([damCenterX, leftPipes[leftPipes.length - 1].level]);
} else {
// No left pipes: Extend from water level to center
saturationPoints.push([damCenterX, waterLevel]);
}
// 3. Handle Right Side
// Connect to the nearest pipe on the right (and subsequent ones)
rightPipes.forEach(p => saturationPoints.push([p.x, p.level]));
// 4. End logic
if (rightPipes.length > 0) {
const lastPipe = rightPipes[rightPipes.length - 1];
if (lastPipe.level < foundationY) {
// If last pipe level is below foundation, extend horizontally to x=250
saturationPoints.push([250, lastPipe.level]);
} else {
// Otherwise, connect to toe
saturationPoints.push(rightToe);
}
} else {
// No right pipes, connect to toe
saturationPoints.push(rightToe);
}
return {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
formatter: () => ''
},
grid: {
left: 60,
right: 60,
top: 60,
bottom: 40
},
xAxis: {
type: 'value',
min: 0,
max: 280,
axisLabel: { color: '#fff' },
splitLine: { show: false }
},
yAxis: {
type: 'value',
min: 0,
max: 200,
axisLabel: { color: '#fff', formatter: '{value}' },
splitLine: {
lineStyle: { type: 'dashed', color: 'rgba(255,255,255,0.1)' }
},
name: '断面高(m)',
nameTextStyle: { color: '#fff', padding: [0, 0, 0, 20] }
},
series: [
// 1. Foundation
{
type: 'custom',
renderItem: (params, api) => {
const points = [[0, 0], [280, 0], [280, foundationY], [0, foundationY]].map(p => api.coord(p));
return {
type: 'polygon',
shape: { points },
style: { fill: foundationColor },
silent: true
};
},
data: [[0, 0]]
},
// 2. Dam Body Shell (Left)
{
type: 'custom',
renderItem: (params, api) => {
const points = [leftToe, leftCrest, [coreTopLeft, coreTopY], [coreBaseLeft, foundationY]].map(p => api.coord(p));
return {
type: 'polygon',
shape: { points },
style: { fill: damColor, stroke: damColor },
silent: true
};
},
data: [[0, 0]]
},
// 3. Dam Body Shell (Right with Berm)
{
type: 'custom',
renderItem: (params, api) => {
const points = [
[coreBaseRight, foundationY],
[coreTopRight, coreTopY],
rightCrest,
bermStart,
bermEnd,
rightToe
].map(p => api.coord(p));
return {
type: 'polygon',
shape: { points },
style: { fill: damColor, stroke: damColor },
silent: true
};
},
data: [[0, 0]]
},
// 4. Core Wall
{
type: 'custom',
renderItem: (params, api) => {
const points = [
[coreBaseLeft, coreBottomY],
[coreTopLeft, coreTopY],
[coreTopRight, coreTopY],
[coreBaseRight, coreBottomY]
].map(p => api.coord(p));
return {
type: 'polygon',
shape: { points },
style: { fill: coreColor, stroke: coreColor },
silent: true
};
},
data: [[0, 0]]
},
// 5. Dam Crest Filler (Top Part)
{
type: 'custom',
renderItem: (params, api) => {
const points = [
[coreTopLeft, coreTopY],
[coreTopRight, coreTopY],
rightCrest,
leftCrest
].map(p => api.coord(p));
return {
type: 'polygon',
shape: { points },
style: { fill: damColor, stroke: damColor },
silent: true
};
},
data: [[0, 0]]
},
// 6. Dam Toe Weight
{
type: 'custom',
renderItem: (params, api) => {
const points = toeWeightPoints.map(p => api.coord(p));
return {
type: 'polygon',
shape: { points },
style: { fill: toeColor, stroke: toeColor },
silent: true
};
},
data: [[0, 0]]
},
// 6. Upstream Water Level (Correctly Filled)
{
type: 'custom',
renderItem: (params, api) => {
const points = [
[0, foundationY],
leftToe, // Join the slope toe
[waterX, waterLevel], // Join the slope at water level
[0, waterLevel]
].map(p => api.coord(p));
return {
type: 'polygon',
shape: { points },
style: { fill: waterColor },
silent: true
};
},
data: [[0, 0]]
},
// 7. Saturation Line
{
type: 'line',
smooth: 0.4,
symbol: 'none',
lineStyle: { color: saturationLineColor, width: 3 },
data: saturationPoints,
z: 11
},
// 8. Flood Level (Dashed Horizontal)
{
type: 'line',
markLine: {
symbol: ['none', 'none'],
label: {
show: true,
position: 'middle',
formatter: `校核洪水位 ${floodLevel}m`,
color: '#fff',
padding: [0, 0, 10, 0]
},
lineStyle: { color: floodLineColor, type: 'dashed', width: 2 },
data: [
[
{ x:70, yAxis: floodLevel },
{ xAxis: floodX, yAxis: floodLevel } // To the dam body
]
]
},
data: []
},
// 9. Red Line from Flood Level to Dam Toe
{
type: 'line',
symbol: 'none',
lineStyle: { color: '#ff0000', width: 2, type: 'solid' }, // Red solid line
data: [
[floodX, floodLevel],
[coreTopLeft, coreTopY],
[coreTopRight, coreTopY],
[237, 50] // To dam toe top
],
z: 10 // Ensure it's on top
},
// 10. Measured Water Level Label
{
type: 'scatter',
symbol: 'triangle',
symbolSize: 10,
symbolRotate: 180,
itemStyle: { color: saturationLineColor },
label: {
show: true,
formatter: `实测水位 ${waterLevel}m`,
position: 'top',
color: '#fff',
fontSize: 12,
offset: [0, 0]
},
data: [[waterX / 2, waterLevel]]
},
// 10. Pipes
{
type: 'custom',
renderItem: renderPipe,
data: pipes.map(p => [p.x, p.top, p.bottom, p.level, p.name]),
z: 12
},
]
};
};

View File

@ -0,0 +1,156 @@
import React, { useState, useEffect } from 'react';
import ReactEcharts from 'echarts-for-react';
import { DatePicker, Select, Button } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import moment from 'moment';
import { getOption } from './chartOption';
import ProcessLineChart from './ProcessLineChart';
import DeformationPanel from './DeformationPanel';
import './index.less';
const { RangePicker } = DatePicker;
const { Option } = Select;
const SafetyPanel = () => {
const [activeTab, setActiveTab] = useState('seepage');
const [filter, setFilter] = useState({
type: 'saturation', // saturation | process
section: 'main_0_086',
date: [moment().subtract(7, 'days'), moment()]
});
const [chartData, setChartData] = useState({});
const [quickDate, setQuickDate] = useState('1m'); // Default to 1 month
useEffect(() => {
// Mock fetching data
// In a real scenario, this would depend on the selected section
setChartData({
waterLevel: 113.8,
floodLevel: 128.42,
pipes: [
{ name: 'UPD4', x: 110, top: 175, bottom: 60, level: 117.67 },
{ name: 'UPD1', x: 160, top: 175, bottom: 60, level: 100.67 },
{ name: 'UPD2', x: 190, top: 150, bottom: 30, level: 40 },
{ name: 'UPD3', x: 210, top: 130, bottom: 20, level: 30 },
]
});
}, [filter]);
const handleTabChange = (key) => {
setActiveTab(key);
};
const handleQuickDate = (type) => {
setQuickDate(type);
let start = moment();
if (type === '1m') start = moment().subtract(1, 'months');
if (type === '6m') start = moment().subtract(6, 'months');
if (type === '1y') start = moment().subtract(1, 'years');
setFilter({ ...filter, date: [start, moment()] });
};
return (
<div className="safety-panel-container">
{/* Sidebar for navigation */}
<div className="sidebar">
<div
className={`sidebar-btn ${activeTab === 'seepage' ? 'active' : ''}`}
onClick={() => handleTabChange('seepage')}
>
渗压监测
</div>
<div
className={`sidebar-btn ${activeTab === 'deformation' ? 'active' : ''}`}
onClick={() => handleTabChange('deformation')}
>
变形监测
</div>
</div>
{/* Main content area */}
<div className="main-content-area">
{/* Filters bar */}
<div className="filters-bar">
{activeTab === 'seepage' ? (
<>
<Select
value={filter.type}
style={{ width: 120, marginRight: 12 }}
onChange={v => setFilter({...filter, type: v})}
dropdownStyle={{ background: '#0d2c4d', color: '#fff' }}
>
<Option value="saturation">浸润线图</Option>
<Option value="process">过程线图</Option>
</Select>
<span className="filter-label">断面:</span>
<Select
value={filter.section}
style={{ width: 150, marginRight: 12 }}
onChange={v => setFilter({...filter, section: v})}
dropdownStyle={{ background: '#0d2c4d', color: '#fff' }}
>
<Option value="main_0_086">主坝0+086</Option>
</Select>
<span className="filter-label">时间:</span>
<RangePicker
value={filter.date}
onChange={v => setFilter({...filter, date: v})}
style={{ width: 260, marginRight: 12 }}
dropdownClassName="dark-picker-dropdown"
/>
<Button className="ant-btn-ghost-blue" icon={<SearchOutlined />} onClick={() => {}}>查询</Button>
</>
) : (
<>
<Select value="过程线图" style={{ width: 120, marginRight: 12 }} disabled dropdownStyle={{ background: '#0d2c4d', color: '#fff' }}>
<Option value="process">过程线图</Option>
</Select>
<RangePicker
value={filter.date}
onChange={v => {
setFilter({...filter, date: v});
setQuickDate('');
}}
style={{ width: 260, marginRight: 12 }}
dropdownClassName="dark-picker-dropdown"
/>
<div className="quick-date-group" style={{ display: 'flex', marginRight: 12 }}>
<Button type={quickDate === '1m' ? 'primary' : 'default'} onClick={() => handleQuickDate('1m')} style={{ borderRadius: '4px 0 0 4px' }}>近一月</Button>
<Button type={quickDate === '6m' ? 'primary' : 'default'} onClick={() => handleQuickDate('6m')} style={{ borderRadius: '0', marginLeft: '-1px' }}>近半年</Button>
<Button type={quickDate === '1y' ? 'primary' : 'default'} onClick={() => handleQuickDate('1y')} style={{ borderRadius: '0 4px 4px 0', marginLeft: '-1px' }}>近一年</Button>
</div>
<Button className="ant-btn-ghost-blue" type="primary" onClick={() => {}}>查询</Button>
</>
)}
</div>
{/* Chart/Table content */}
<div className="chart-content">
{activeTab === 'seepage' ? (
filter.type === 'saturation' ? (
<ReactEcharts
option={getOption(chartData)}
style={{ height: '100%', width: '100%' }}
notMerge={true}
lazyUpdate={true}
/>
) : (
<ProcessLineChart />
)
) : (
<DeformationPanel />
)}
</div>
</div>
</div>
);
};
export default SafetyPanel;

View File

@ -0,0 +1,67 @@
.safety-panel-container {
width: 100%;
height: 100%;
display: flex;
padding: 16px;
color: #fff;
.sidebar {
width: 140px;
display: flex;
flex-direction: column;
gap: 15px;
.sidebar-btn {
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #fff;
font-size: 14px;
border: 1px solid rgba(0, 160, 233, 0.6);
background: rgba(0, 70, 120, 0.3);
transition: all 0.3s;
&:hover {
background: rgba(0, 160, 233, 0.3);
}
&.active {
background: rgba(0, 160, 233, 0.8);
border-color: #00a0e9;
box-shadow: 0 0 10px rgba(0, 160, 233, 0.4);
}
}
}
.main-content-area {
flex: 1;
margin-left: 20px;
display: flex;
flex-direction: column;
position: relative;
.filters-bar {
display: flex;
align-items: center;
margin-bottom: 16px;
.filter-label {
margin-right: 8px;
color: #fff;
}
}
.chart-content {
flex: 1;
position: relative;
overflow: hidden; // Prevent overflow
.echarts-for-react {
height: 100% !important;
width: 100% !important;
}
}
}
}

View File

@ -8,14 +8,9 @@ import './index.less';
import RainMonitor from './RainMonitor';
import ReservoirPanel from './ReservoirPanel';
import FlowPanel from './FlowPanel';
import SafetyPanel from './SafetyPanel';
const { RangePicker } = DatePicker;
const SafetyPanel = () => {
return (
<div className="awm-empty">内容待接入</div>
);
};
const AllWeatherModal = ({ active }) => {
if (active === 'rain') return <RainMonitor />;
if (active === 'reservoir') return <ReservoirPanel />;