Compare commits

...

5 Commits

98 changed files with 6477 additions and 220 deletions

View File

@ -1,2 +1,2 @@
GENERATE_SOURCEMAP=false GENERATE_SOURCEMAP=false
PUBLIC_URL=/ssDp PUBLIC_URL=/ssDp

View File

@ -25,7 +25,8 @@
"craco-less": "2.1.0-alpha.0", "craco-less": "2.1.0-alpha.0",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"echarts": "^4.9.0", "echarts": "^4.9.0",
"echarts-for-react": "^3.0.2", "echarts-for-react": "3.0.2",
"ezuikit-js": "8.0.4-beta.1",
"http-proxy-middleware": "^2.0.6", "http-proxy-middleware": "^2.0.6",
"moment": "^2.29.4", "moment": "^2.29.4",
"react": "^18.2.0", "react": "^18.2.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 B

BIN
public/assets/images/jk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -0,0 +1,76 @@
import React,{useEffect,useState} from 'react'
import {Slider} from "antd"
import './index.less'
export default function VideoControler({ selectItem = {},onOperation }) {
const ctrObj = {
1: {name:"左上",value:'LEFT_UP'},
2: {name:"上",value:'UP'},
3: {name:"右上",value:'RIGHT_UP'},
4: {name:"左",value:'LEFT'},
5: {name:"重置",value:'GOTO_PRESET'},
6: {name:"右",value:'RIGHT'},
7: {name:"左下",value:'LEFT_DOWN'},
8: {name:"下",value:'DOWN'},
9: {name:"右下",value:'RIGHT_DOWN'},
}
const ctrBtnArr = Array(9).fill(0).map((item, index) => ({
title: ctrObj[index + 1]?.name,
value:ctrObj[index + 1]?.value
}))
const marks = {
10: "1",
30: "3",
80: "8",
}
const [selectData, setSelect] = useState()
useEffect(() => {
if (selectData) {
onOperation({...selectData,speed:selectData?.speed || 30})
}
}, [selectData])
return (
<div className='controler-wrap'>
<div className='title'>云台控制</div>
<div >
<span className='sub-title'>当前设备</span>
<span >{selectItem?.name}</span>
</div>
{/* <div className='sub-title-name'>{selectItem?.name}</div> */}
<div className='controler-box'>
<div className='left'>
{ctrBtnArr.map((item, index) =>
<div key={index} onClick={() => setSelect({...selectData,command:item?.value})}>
<div className='ctr-btn' title={item?.title}>
<img alt='' src={`${process.env.PUBLIC_URL}/assets/images/crt${index}.png`} />
</div>
</div>
)}
</div>
<div className='right'>
<div className='slider-wrap'>
<Slider
marks={marks}
defaultValue={30}
step={10}
min={10}
max={80}
onChange={(e) => {setSelect({...selectData,speed:e})}}
/>
</div>
<div className='zoom-wrap'>
<div className='zoom-btn' title='缩小' onClick={() => setSelect({...selectData,command:"ZOOM_OUT"})}>
<img alt='' src={`${process.env.PUBLIC_URL}/assets/images/suoxiao.png`} style={{width:25,height:25}}/>
</div>
<div className='zoom-btn' title='放大' onClick={() => setSelect({...selectData,command:"ZOOM_IN"})}>
<img alt='' src={`${process.env.PUBLIC_URL}/assets/images/fangda.png`} />
</div>
</div>
</div>
<p className='tips'>云台操作的生效根据网络状况延时约1-10</p>
</div>
</div>
)
}

View File

@ -0,0 +1,120 @@
.controler-wrap{
width: 100%;
height: 265px;
padding-left: 5px;
color: #fff;
.title{
line-height: 50px;
font-size: 25px;
font-weight: 900;
color: #fff;
}
.sub-title{
font-size: 18px;
font-weight:800;
color: #fff;
}
.controler-box{
display: flex;
height: 170px;
padding: 10px 20px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
flex-wrap: wrap;
.left{
width: 135px;
display: flex;
flex-wrap: wrap;
.ctr-btn{
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 5px;
margin-right: 5px;
margin-bottom: 5px;
width: 40px;
height: 40px;
display: flex;
-webkit-box-pack: center;
justify-content: center;
-webkit-box-align: center;
align-items: center;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: rgba(24, 144, 255, 0.3);
border-color: #1890ff;
}
img{
width: 15px;
height: 15px;
filter: invert(1);
}
}
}
.right{
width:calc(100% - 135px);
.slider-wrap{
// padding: 10px;
height: 40px;
// margin-bottom: 5px;
.ant-slider-mark-text {
color: #fff !important ;
}
.ant-slider-rail {
background-color: rgba(255, 255, 255, 0.2);
}
.ant-slider-track {
background-color: #1890ff;
}
.ant-slider-handle {
border-color: #1890ff;
}
}
.zoom-wrap{
display: flex;
-webkit-box-pack: justify;
// justify-content: space-between;
width: 100%;
margin-top: -3px;
.zoom-btn{
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 5px;
margin-right: 5px;
margin-bottom: 5px;
width: 100%;
height: 40px;
-webkit-box-pack: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: rgba(24, 144, 255, 0.3);
border-color: #1890ff;
}
img{
width: 25px;
height: 25px;
filter: invert(1);
}
}
}
}
.tips{
margin-top: 0;
margin-bottom: 1rem;
color: rgba(255, 255, 255);
font-size: 12px;
}
}
}

View File

@ -0,0 +1,243 @@
import { FC, useEffect, useRef, useState } from 'react'
// import styles from './index.module.less'
import { message, Spin } from "antd";
import EZUIKit from 'ezuikit-js';
import moment from 'moment';
/**
* 海康视频H5插件视频播放
* @author QC班长
* @since 20230727
*/
//wsUrl [{src:''}] playerId:string size:分屏播放1代表1*12代表2*2 最大4*4
const HFivePlayer = ({ wsUrl, playerID, size }) => {
const [isLoading, setIsLoading] = useState(false)
const player = useRef({})//播放器
const playerYsy = useRef({})
const parentRef = useRef(null);//为了设置播放器宽高
const [width, setWidth] = useState(null);
const [height, setHeight] = useState(null);
const [type, setType] = useState(false);
const initVideo = () => {
const hasReplayRange = wsUrl?.beginTime && wsUrl?.endTime && wsUrl?.indexCode;
const beginStr = hasReplayRange ? moment(wsUrl.beginTime).format('YYYYMMDDHHmmss') : '';
const endStr = hasReplayRange ? moment(wsUrl.endTime).format('YYYYMMDDHHmmss') : '';
const ezUrl = hasReplayRange
? `ezopen://open.ys7.com/${wsUrl.indexCode}/1.rec?begin=${beginStr}&end=${endStr}`
: `ezopen://open.ys7.com/${wsUrl.indexCode}/1.live`;
playerYsy.current = new EZUIKit.EZUIKitPlayer({
id: 'player' + playerID, // 视频容器ID
accessToken: wsUrl.src,
url: ezUrl,
// plugin: ["talk"], // 加载插件talk-对讲
width: parentRef.current?.offsetWidth,
height: parentRef.current?.offsetHeight,
template: 'simple',
footer: ['hd', 'fullScreen'],
});
playerYsy.current?.play();
}
/**
* 初始化播放器
*/
const initPlayer = () => {
console.log(document.getElementById('player' + playerID),'play.current');
// return
player.current = new window.JSPlugin({
// 需要英文字母开头 必填
szId: 'player' + playerID,
// 必填,引用H5player.min.js的js相对路径
szBasePath: '/h5Player/',
// 当容器div#play_window有固定宽高时可不传iWidth和iHeight窗口大小将自适应容器宽高
iWidth: parentRef.current?.offsetWidth,
iHeight: parentRef.current?.offsetHeight,
bSupporDoubleClickFull: true,
// 分屏播放默认最大分屏4*4
iMaxSplit: 3,
// iCurrentSplit: 1,
// 样式
oStyle: {
border: 'rgb(53 116 237)',
borderSelect: '#1d325d',
background: '#1d325d',
}
})
// 设置播放容器的宽高并监听窗口大小变化
//初始化插件
initPlugin()
}
/**
* 事件初始化
*/
const initPlugin = () => {
player.current.JS_SetWindowControlCallback({
windowEventSelect(iWindIndex) {
// 插件选中窗口回调
console.log('windowSelect callback: ', iWindIndex)
//点击视频全屏显示
// wholeFullScreen()
},
pluginErrorHandler(iWindIndex, iErrorCode, oError) {
// 插件错误回调
// console.error(`window-${iWindIndex}, errorCode: ${iErrorCode}`, oError)
// message.error('播放失败1:' + VideoPlayerException[iErrorCode])
setIsLoading(false)
//重新播放
// initPlayer()
},
windowEventOver(iWindIndex) {
// 鼠标移过回调
// console.log('鼠标移过回调', iWindIndex)
},
windowEventOut(iWindIndex) {
// 鼠标移出回调
// console.log('鼠标移出回调', iWindIndex)
},
windowFullScreenChange(bFull) {
// 全屏切换回调
// console.log('全屏切换回调', bFull)
},
firstFrameDisplay(iWndIndex, iWidth, iHeight) {
// 首帧显示回调
// console.log('首帧显示回调', iWndIndex, iWidth, iHeight)
//停止加载
setIsLoading(false)
},
performanceLack(iWndIndex) {
// 性能不足回调
console.log('性能不足回调', iWndIndex)
setIsLoading(false)
}
})
console.log(player, '成功');
//播放
play()
}
const handleWindowResize = () => {
console.log(111111);
if (parentRef.current) {
setWidth(parentRef.current?.offsetWidth)
setHeight(parentRef.current?.offsetHeight)
}
console.log(parentRef.current?.offsetHeight, parentRef.current?.offsetWidth);
}
/**
* 播放
*/
const play = () => {
console.log(wsUrl?.src, '6543');
// if(!wsUrl?.src) return
if (wsUrl?.src) {
setIsLoading(true) //开始加载
let preUrl = wsUrl?.src // 播放地址
// 支持回放时间范围(海康取流地址若后端支持时间参数则追加)
if (wsUrl?.beginTime && wsUrl?.endTime) {
const begin = moment(wsUrl.beginTime).subtract(1,'hours').format('YYYY-MM-DD HH:mm:ss');
const end = moment(wsUrl.endTime).format('YYYY-MM-DD HH:mm:ss');
const sep = preUrl.includes('?') ? '&' : '?';
preUrl = `${preUrl}${sep}beginTime=${begin}&endTime=${end}`;
}
console.log(preUrl);
const param = {
playURL: preUrl,
// 1高级模式 0普通模式高级模式支持所有
mode: 0
}
// 当前播放窗口下标
// let index = 0
player.current.JS_Play(preUrl, param, 0).then(() => {
// 播放成功回调
// console.log('播放成功')
}, (err) => {
// console.log('播放失败')
// console.info('JS_Play failed:', err)
setIsLoading(false)
message.error('视频离线,播放失败')
}
)
}
}
useEffect(() => {
if (wsUrl) {
if (wsUrl?.relType == 'ysy') {
initVideo()
} else {
initPlayer()
}
}
}, [wsUrl])
useEffect(() => {
handleWindowResize()
}, [size])
const styles = {}
return (
<div ref={parentRef} style={{ height: '100%', width: '100%' }}>
<Spin tip="Loading" spinning={isLoading} size="small">
<div style={{ width: width, height: height }} onDoubleClick={() => playerYsy.current.fullScreen()}>
<div id={'player' + playerID} style={{ width: width, height: height}} >
</div>
</div>
</Spin>
</div>
)
}
export default HFivePlayer
/**
* 海康威视视频播放异常错误代码常量
*/
export const VideoPlayerException = {
'0x12f900001': '接口调用参数错误',
'0x12f900002': '不在播放状态',
'0x12f900003': '仅回放支持该功能',
'0x12f900004': '普通模式不支持该功能',
'0x12f900005': '高级模式不支持该功能',
'0x12f900006': '高级模式的解码库加载失败',
'0x12f900008': 'url格式错误',
'0x12f900009': '取流超时错误',
'0x12f900010': '设置或者是获取音量失败,因为没有开启音频的窗口',
'0x12f900011': '设置的音量不在1-100范围',
'0x12f910000': 'websocket连接失败请检查网络是否通畅URL是否正确',
'0x12f910010': '取流失败',
'0x12f910011': '流中断,电脑配置过低,程序卡主线程都可能导致流中断',
'0x12f910014': '没有音频数据',
'0x12f910015': '未找到对应websocket取流套接字被动关闭的报错',
'0x12f910016': 'websocket不在连接状态',
'0x12f910017': '不支持智能信息展示',
'0x12f910018': 'websocket长时间未收到message',
'0x12f910019': 'wss连接失败原因端口尚未开通、证书未安装、证书不安全',
'0x12f910020': '单帧回放时不能暂停',
'0x12f910021': '已是最大倍速',
'0x12f910022': '已是最小倍速',
'0x12f910023': 'ws/wss连接超时默认6s超时时间原因网络异常网络不通',
'0x12f910026': 'jsdecoder1.0解码报错视频编码格式不支持',
'0x12f910027': '后端取流超时主动关闭连接设备突然离线或重启网络传输超时20s',
'0x12f910028': '设置的缓冲区大小无效大小0-510241024不在该范围的报错',
'0x12f910029': '普通模式的报错,码流异常导致黑屏,尝试重新取流',
'0x12f910031': '普通模式下播放卡主会出现',
'0x12f910032': '码流编码格式普通模式下不支持,可切换高级模式尝试播放',
'0x12f920015': '未调用停止录像,再次调用开始录像',
'0x12f920016': '未开启录像调用停止录像接口错误',
'0x12f920017': '紧急录像目标格式不支持非ps/mp4',
'0x12f920018': '紧急录像文件名为null',
'0x12f930010': '内存不足',
'0x12f930011': '首帧显示之前无法抓图,请稍后重试',
'0x12f950000': '采集音频失败可能是在非https域下使用对讲导致',
'0x12f950001': '对讲不支持这种音频编码格式',
}

View File

@ -238,6 +238,22 @@ input:-webkit-autofill:active {
&:not(.ant-select-disabled):hover .ant-select-selector { &:not(.ant-select-disabled):hover .ant-select-selector {
border-color: #3b7cff !important; border-color: #3b7cff !important;
} }
// Multiple Select Tags
&.ant-select-multiple .ant-select-selection-item {
background: rgba(0, 160, 233, 0.2); // Light blue transparent
border: 1px solid #00a0e9;
color: #fff;
margin-top: 2px;
margin-bottom: 2px;
.ant-select-selection-item-remove {
color: rgba(255, 255, 255, 0.7);
&:hover {
color: #fff;
}
}
}
} }
// Dropdowns (Select, DatePicker, etc.) // Dropdowns (Select, DatePicker, etc.)
@ -269,16 +285,56 @@ input:-webkit-autofill:active {
th { color: #fff; } th { color: #fff; }
td { color: rgba(255, 255, 255, 0.8); } 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 { .ant-picker-cell {
color: rgba(255, 255, 255, 0.5); // Default inactive color
&:hover .ant-picker-cell-inner { &:hover .ant-picker-cell-inner {
background: rgba(0, 160, 233, 0.3); background: rgba(0, 160, 233, 0.3) !important;
} }
&-in-view { &-in-view {
color: #fff; // Active month days
}
// Fix: Remove white background from cells
.ant-picker-cell-inner {
background: transparent !important;
color: inherit;
}
&-selected .ant-picker-cell-inner {
background: #00a0e9 !important; // Project Cyan
color: #fff; color: #fff;
} }
&-selected .ant-picker-cell-inner {
background: #3b7cff !important; &-today .ant-picker-cell-inner {
border: 1px solid #00a0e9;
&::before {
border: 1px solid #00a0e9 !important;
}
}
// Range Selection
&-in-range::before {
background: rgba(0, 160, 233, 0.2) !important;
}
&-range-start .ant-picker-cell-inner,
&-range-end .ant-picker-cell-inner {
background: #00a0e9 !important;
color: #fff;
} }
} }
@ -308,14 +364,76 @@ input:-webkit-autofill:active {
// 4. Button Component (Ghost Blue Style) // 4. Button Component (Ghost Blue Style)
// ============================================================================== // ==============================================================================
.ant-btn-ghost-blue { .ant-btn-ghost-blue {
background: rgba(24, 144, 255, 0.3) !important; // Slight blue tint background background: rgba(18, 56, 102, 0.6) !important;
border: 1px solid #1890ff !important; border: 1px solid #00a0e9 !important;
color: #fff !important; color: #fff !important;
box-shadow: 0 0 8px rgba(24, 144, 255, 0.3) inset; box-shadow: 0 0 8px rgba(0, 160, 233, 0.2) inset;
}
.ant-btn.ant-btn-ghost-blue,
.ant-btn.ant-btn-primary.ant-btn-ghost-blue {
background: rgba(18, 56, 102, 0.6) !important;
border-color: #00a0e9 !important;
color: #fff !important;
}
.ant-btn.ant-btn-ghost-blue:hover,
.ant-btn.ant-btn-ghost-blue:focus,
.ant-btn.ant-btn-primary.ant-btn-ghost-blue:hover,
.ant-btn.ant-btn-primary.ant-btn-ghost-blue:focus {
background: rgba(0, 160, 233, 0.3) !important;
border-color: #33b5ed !important;
color: #fff !important;
}
// ==============================================================================
// 5. Timeline Component
// ==============================================================================
.ant-timeline {
color: #fff;
&:hover, &:focus { .ant-timeline-item-label {
background: rgba(24, 144, 255, 0.3) !important; color: #fff;
border-color: #40a9ff !important; width: calc(50% - 12px);
color: #fff !important; }
.ant-timeline-item-tail {
border-left: 2px solid rgba(0, 160, 233, 0.3);
}
.ant-timeline-item-head {
background-color: transparent;
border-color: #00a0e9;
}
.ant-timeline-item-head-blue {
color: #00a0e9;
border-color: #00a0e9;
}
}
// ==============================================================================
// 6. Button Component (Global Override for Dark Theme)
// ==============================================================================
.ant-btn {
&.ant-btn-primary {
background: #00a0e9; // Project Theme Cyan
border-color: #00a0e9;
&:hover, &:focus {
background: #33b5ed;
border-color: #33b5ed;
}
}
// Default button style in dark mode
&:not(.ant-btn-primary):not(.ant-btn-link):not(.ant-btn-text):not(.ant-btn-danger) {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.3);
color: #fff;
&:hover, &:focus {
color: #00a0e9;
border-color: #00a0e9;
}
} }
} }

4
src/config/index.js Normal file
View File

@ -0,0 +1,4 @@
export const config = {
ip: 'http://223.75.53.141:83',
minioIp:"http://223.75.53.141:9100/gs-ss"
}

View File

@ -38,11 +38,11 @@ code {
background: transparent; // Override white background background: transparent; // Override white background
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #ECF2F9; background: rgba(0, 160, 233, 0.5);
border-radius: 4px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #ECF2F9; background: rgba(0, 160, 233, 0.8);
} }
@ -58,10 +58,10 @@ code {
} }
} }
.ant-table-content::-webkit-scrollbar-thumb,.ant-table-body::-webkit-scrollbar-thumb { .ant-table-content::-webkit-scrollbar-thumb,.ant-table-body::-webkit-scrollbar-thumb {
background: #C1C1C1;//#ECF2F9; background: rgba(0, 160, 233, 0.5);//#ECF2F9;
} }
.ant-table-content::-webkit-scrollbar-thumb,.ant-table-body::-webkit-scrollbar-thumb:hover { .ant-table-content::-webkit-scrollbar-thumb,.ant-table-body::-webkit-scrollbar-thumb:hover {
background: #C1C1C1;//#ECF2F9; background: rgba(0, 160, 233, 0.8);//#ECF2F9;
} }
.ant-modal-title{ .ant-modal-title{
@ -70,68 +70,7 @@ code {
} }
.adcdTreeSelectorBox{
width: 230px;
height:calc( 100vh - 145px );
margin:0 20px 0 0;
background: #fff;
//box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
position:relative;
border: 1px solid #d9d9d9;
box-sizing: border-box;
.treeBox{
//height:calc( 100vh - 158px );
overflow: hidden auto;
padding: 8px 0 0 2px;
border-top: 1px solid #d9d9d9;
}
.ant-input-group .ant-input,.ant-btn{
border:0;
}
.ant-input-affix-wrapper{
border:0;
//border-right: 2px solid #d9d9d9;
}
.checkboxBox{
position:absolute;
top:34px;
width:100%;
height:30px;
background:#fff;
border-top: 1px solid #d9d9d9;
padding-top:4px;
}
}
.adcdTreeTableBox{
width:calc( 100vw - 602px );
height:calc( 100vh - 143px );
background: #fff;
//box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
}
.CrudAdcdTreeTableBox{
width:calc( 100vw - 602px );
height:calc( 100vh - 143px );
background: #fff;
//box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
}
.radarVideoModal{
.ant-modal-content{
width: 100%;
height: 100%;
.ant-modal-body{
width: 100%;
height: calc( 100% - 120px );
padding: 0 30px;
}
.ant-tabs-tabpane{
height: calc( 76vh - 120px );
}
}
}
.toolbarBox{ .toolbarBox{
.ant-form-item{ .ant-form-item{

View File

@ -7,6 +7,24 @@ const apiurl = {
router: service + '/getRouters', router: service + '/getRouters',
role: service + '/system/menu/list' role: service + '/system/menu/list'
}, },
station: {
rainlist: service + '/real/rain/list',//雨量站
reservoirlist: service + '/reservoir/water/listV2',//水库水位站
flowlist: service + '/stFlowR/list',
},
spjk: {
page1: service + "/iscaiEvent/page",
page: service + "/stImgWarnR/page",
list: service + "/attCctvBase/list",
controler: service + "/attCctvBase/control",
treeList: service + '/cctvBMenu/list',
treeListById: service + '/xfCctvB/listByMenuId/',
srcData: service + '/attCctvBase/preview/',
videoBystcd: service + '/stbprp/cctv/listByStcd/',
videoList: service + '/attCctvBase/list',
ysyToken: service + '/ysy/getAccessToken'
},
sq: { sq: {
qfg: { qfg: {
info:service + '/attResBase/list' info:service + '/attResBase/list'
@ -14,7 +32,45 @@ const apiurl = {
qys: { qys: {
yhjmhPage:service + '/iaCFlrvvlg/page', yhjmhPage:service + '/iaCFlrvvlg/page',
qsydwPage:service + '/iaCBsnssinfo/page', qsydwPage:service + '/iaCBsnssinfo/page',
wxqPage:service + '/iaCDanad/page', wxqPage: service + '/iaCDanad/page',
gcys: {
buildInfo: service + '/attResBuilding/info',
krlineList:service + '/stZvarlB/list',
xllineList:service + '/stZqrlB/list',
}
},
qth: {
rainList: {
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',
monitor: service + '/reservoir/water/monitor/data',
detail:service + '/reservoir/water/detail'
},
flow: {
history: service + '/stFlowR/upperDataCheck',
max:service + '/stFlowR/lowerDataCheck'
}
},
qzq: {
list: service + '/projectEvents/doc/page',
export: service + '/projectEvents/export',
info :service + '/wholeCycle/get'
}
},
sz: {
jqjz: {
budgetInfo: service + '/fundBudget/get/',
manageInfo: service + '/screen/mechanisms/equipment',
managePic: service + '/screen/manageHouseImg/get',
wzPage:service + '/rescue/goods/page/query'
} }
} }
} }

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

@ -0,0 +1,54 @@
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||[];
}
// 流量站
export async function flowlist(params) {
const {data, code, msg} = await httpget(apiurl.station.flowlist, 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

@ -49,6 +49,15 @@ async function send(url, options) {
return {}; return {};
} }
async function sendFile(url, options) {
try {
const res = await request(url, options,'blob');
return res;
} catch (e) {
message.error(e);
}
return {};
}
export function httpget(url, data = {}) { export function httpget(url, data = {}) {
const params = []; const params = [];
@ -106,7 +115,7 @@ export function ry_httpget(url, data = {}) {
} }
export function httppost(url, data = {}) { export function httppost(url, data = {},type) {
const options = { const options = {
method: 'POST', method: 'POST',
headers: { headers: {
@ -117,8 +126,8 @@ export function httppost(url, data = {}) {
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}; };
const fun = type == 'blob' ? sendFile(url, options) : send(url, options);
return send(url, options); return fun;
} }
export function httpPostFile(url, data = {}) { export function httpPostFile(url, data = {}) {

View File

@ -19,6 +19,22 @@ export function objType(obj) {
} }
return typeof obj === 'object' || typeof obj === 'function' ? class2type[toString.call(obj)] || 'object' : typeof obj; return typeof obj === 'object' || typeof obj === 'function' ? class2type[toString.call(obj)] || 'object' : typeof obj;
} }
export const exportFile = (name, res) => {
// 创建a标签
let blob = new Blob([res],{ type: 'application/octet-stream' })
// 创建下载链接
const downloadUrl = URL.createObjectURL(blob);
// 创建下载链接的 <a> 元素
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = name;
// 模拟点击下载链接
downloadLink.click();
// 清理创建的 URL 对象
URL.revokeObjectURL(downloadUrl);
}
/** /**
* 是否数组 * 是否数组
* @param {object} obj 需要判断的对象 * @param {object} obj 需要判断的对象

View File

@ -0,0 +1,223 @@
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 './index.less';
import RightPanel from '../ModalComponents/AllWeatherModal/RainMonitor/RightPanel';
import ReservoirPanel from '../ModalComponents/AllWeatherModal/ReservoirPanel';
import VideoList from '../ModalComponents/VideoList'
import UAVModal from '../ModalComponents/UAVModal';
const AllWeatherControl = () => {
const [reservoirItem, setReservoirItem] = useState({})
const [rainList, setRainList] = useState([])
const [detailVisible, setDetailVisible] = useState(false)
const [reservoirVisible, setReservoirVisible] = useState(false)
const [videoOpen, setVideoOpen] = useState(false)
const [selectedStcd, setSelectedStcd] = useState(null)
// UAV Modal States
const [uavModalVisible, setUavModalVisible] = useState(false);
const [uavActiveTab, setUavActiveTab] = useState('1');
const uavTabs = [
{ label: '直播画面', value: '1' },
{ label: '飞行任务', value: '2' },
{ label: '历史记录', value: '3' }
];
const rainColumns = [
{
title: '站名',
dataIndex: 'stnm',
key: 'stnm',
align: 'center',
width: 140,
ellipsis: true
},
{
title: '今日',
dataIndex: 'today',
key: 'today',
align: 'center',
},
{
title: '昨日',
dataIndex: 'yesterdayDrp',
key: 'yesterdayDrp',
align: 'center',
},
{
title: '24h预报',
dataIndex: 'h24',
key: 'h24',
align: 'h24',
},
];
// Reservoir Data
const reservoirData = [
{ label: '主坝坝前', value: reservoirItem?.rz, unit: 'm', upArrow: reservoirItem?.status > 0, underline: true,downArrow:reservoirItem?.status < 0, clickable: true },
{ label: '汛限水位', value: reservoirItem?.flLowLimLev, unit: 'm' },
{ label: '距汛限', value: reservoirItem?.gapFlLowLimLev, unit: 'm', isNegative: reservoirItem?.gapFlLowLimLev < 0 },
{ label: '副坝坝前', value: reservoirItem?.rz, unit: 'm', clickable: true,underline: true },
{ 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.list);
if (result.code == 200) {
setRainList(result.data)
}
} catch (error) {
console.log(error);
}
}
useEffect(() => {
getRainList()
getReservoir()
}, [])
const openUavModal = (key) => {
setUavActiveTab(key);
setUavModalVisible(true);
}
return (
<div className="all-weather-control">
{/* 雨情 Section */}
<div className="section rain-section">
<div className="section-header">
<div className="title-wrapper">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span className="section-title">雨情</span>
</div>
</div>
<Table
columns={rainColumns}
dataSource={rainList}
pagination={false}
size="small"
rowKey={'stcd'}
rowClassName={(record) => record.isTotal ? 'total-row' : ''}
bordered={false}
scroll={{ y: 300 }}
onRow={(record) => ({
onClick: () => {
setSelectedStcd(record);
setDetailVisible(true);
}
})}
/>
<CommonModal
title={selectedStcd?.stnm}
visible={detailVisible}
onClose={() => setDetailVisible(false)}
width={'70%'}
>
<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>
<div className="reservoir-cards">
{reservoirData.map((item, index) => (
<div
key={index}
className="reservoir-card"
style={{ backgroundImage: `url(${smallCard})`, cursor: item.clickable ? 'pointer' : 'default' }}
onClick={() => {
if (item.clickable) {
setReservoirVisible(true)
}
}}
>
<div className={`value ${item.isPrimary ? 'primary' : ''} ${item.label == '距汛限'?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.upArrow && <span className="arrow-up"></span>}
{item.downArrow && <span className="arrow-down"></span>}
</div>
))}
</div>
<CommonModal
title={reservoirItem?.stnm || '水库水情'}
visible={reservoirVisible}
onClose={() => setReservoirVisible(false)}
width={'90%'}
>
<ReservoirPanel stcd={reservoirItem?.stcd} cleanMode={true} />
</CommonModal>
</div>
{/* 无人机 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" onClick={()=>setVideoOpen(true)}>视频墙</span>
</div>
<div className="uav-content">
<div
className="uav-image"
style={{ backgroundImage: `url(${wrj})` }}
/>
<div className="uav-actions">
<div className="uav-button" onClick={() => openUavModal('1')}>直播画面</div>
<div className="uav-button" onClick={() => openUavModal('2')}>巡查任务</div>
</div>
</div>
<CommonModal
title={'视频监控'}
visible={videoOpen}
onClose={() => setVideoOpen(false)}
width={'80%'}
>
<VideoList />
</CommonModal>
<CommonModal
title={'无人机巡查'}
visible={uavModalVisible}
onClose={() => setUavModalVisible(false)}
width={'70%'}
tabs={uavTabs}
activeTab={uavActiveTab}
onTabChange={setUavActiveTab}
>
<UAVModal activeKey={uavActiveTab} />
</CommonModal>
</div>
</div>
);
};
export default AllWeatherControl;

View File

@ -0,0 +1,214 @@
.all-weather-control {
height: 100%;
display: flex;
flex-direction: column;
color: #fff;
padding: 5px;
overflow-y: auto;
// Global Scrollbar
&::-webkit-scrollbar {
width: 0;
height: 0;
}
.section {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.title-wrapper {
display: flex;
align-items: center;
.arrow-icon {
width: 20px;
height: 18px;
margin-right: 8px;
object-fit: contain;
}
.section-title {
font-size: 16px;
color: #fff; // Matches the orange/gold color in screenshot
font-weight: 500;
}
}
.link {
color: #00eaff;
font-size: 14px;
cursor: pointer;
text-decoration: underline;
&:hover {
color: #fff;
}
}
}
}
// Rain Table Styling
.rain-section {
.ant-table-wrapper {
.ant-table {
background: transparent;
color: #fff;
.ant-table-thead > tr > th {
background: rgba(0, 70, 110, 0.6);
color: #fff;
border-bottom: none;
padding: 8px 4px;
text-align: center;
font-size: 13px;
}
.ant-table-tbody > tr > td {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
padding: 8px 4px;
text-align: center;
font-size: 13px;
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.1) !important;
}
// Zebra striping if needed, or just transparent
.ant-table-tbody > tr.ant-table-row:nth-child(even) {
background-color: rgba(255, 255, 255, 0.05);
}
}
.ant-table-placeholder {
background: transparent;
.ant-empty-description {
color: rgba(255,255,255,0.5);
}
}
}
}
.reservoir-section {
.reservoir-cards {
display: flex;
flex-wrap: wrap;
.reservoir-card {
width: calc((100% - 20px) / 3);
margin-bottom: 5px;
margin-right: 10px;
&:nth-child(3n) {
margin-right: 0;
}
background-size: 100% 100%;
background-repeat: no-repeat;
height: 60px; // Adjust based on design
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 5px;
position: relative;
.value {
font-size: 18px;
font-weight: bold;
color: #00D8FF;
.num {
display: inline-block;
padding-bottom: 2px;
}
.num.underline {
border-bottom: 1px solid #00a0e9;
}
.unit {
font-size: 12px;
font-weight: normal;
margin-left: 2px;
color: #fff;
}
&.positive { color: #ff4d52; }
&.negative { color: #68c639; }
}
.label {
font-size: 14px;
color: #9DD2E4;
margin-top: 2px;
}
.arrow-up,.arrow-down {
color: #ff4d4f;
position: absolute;
right: 5px;
top: 5px;
}
.arrow-down{
color: #68c639;
}
}
}
}
// UAV Styling
.uav-section {
.uav-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100px;
position: relative;
.uav-image {
flex: 1;
height: 100%;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.uav-actions {
display: flex;
flex-direction: column;
gap: 15px;
margin-left: 10px;
.uav-button {
width: 120px;
height: 36px;
line-height: 36px;
text-align: center;
background: rgba(18, 56, 102, 0.6);
border: 1px solid #00a0e9;
border-radius: 4px;
color: #fff;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 0 5px rgba(0, 160, 233, 0.3);
&:hover {
background: rgba(0, 160, 233, 0.4);
box-shadow: 0 0 10px rgba(0, 160, 233, 0.6);
}
}
}
// Connecting lines visualization (optional, if needed to match 1:1 perfectly)
.connection-line {
// ...
}
}
}
}

View File

@ -0,0 +1,97 @@
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: 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) => {
if (!item.fileId) return;
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">
{data.map((item, index) => (
<div
key={index}
className="cycle-card"
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>
</div>
<div className="label">{item.label}</div>
</div>
))}
</div>
<PdfView
visible={pdfVisible}
title={pdfConfig.title}
url={pdfConfig.url}
fileId={pdfConfig.fileId}
onClose={() => setPdfVisible(false)}
/>
</div>
);
};
export default ManagementCycle;

View File

@ -0,0 +1,59 @@
.management-cycle {
height: 100%;
padding: 10px;
overflow-y: auto;
// Global Scrollbar
&::-webkit-scrollbar {
width: 0;
height: 0;
}
.card-grid {
display: flex;
flex-wrap: wrap;
height: 100%;
align-content: flex-start;
.cycle-card {
width: calc((100% - 20px) / 3);
height: 70px; // Slightly taller to accommodate text comfortably
margin-bottom: 15px;
margin-right: 10px;
&:nth-child(3n) {
margin-right: 0;
}
background-size: 100% 100%;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 5px;
box-sizing: border-box;
.value-wrapper {
margin-bottom: 5px;
&.underlined {
border-bottom: 1px solid #00a0e9; // Match the blue theme
padding-bottom: 2px;
}
.value {
font-size: 18px;
font-weight: bold;
color: #00D8FF;
}
}
.label {
font-size: 14px;
color: #fff;
text-align: center;
}
}
}
}

View File

@ -0,0 +1,135 @@
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 { httppost } from '@/utils/request';
import moment from 'moment';
import flowOption from './chartOption';
import './index.less';
const { RangePicker } = DatePicker;
export default function RightPanel({ stcd, cleanMode = false }) {
const defaultRange = [
moment().subtract(7, 'days').set({ minute: 0, second: 0 }),
moment().set({ minute: 0, second: 0 }),
];
const [dates, setDates] = useState(defaultRange);
const [historyList, setHistoryList] = useState([]);
const [maxInfo, setMaxInfo] = useState({})
const [loading, setLoading] = useState(false);
const columns = [
{
title: '时间', dataIndex: 'tm', key: 'tm', align: 'center', width: 160,
render: (text) => moment(text).format('YYYY-MM-DD HH:mm')
},
{
title: '瞬时流量(m³/s)', dataIndex: 'q', key: 'q', align: 'center',
},
{
title: '累计水量(万m³)', dataIndex: 'totalWater', key: 'totalWater', align: 'center',
},
];
const getHistoryList = async (params) => {
setLoading(true);
try {
const { data, code } = await httppost(apiurl.sq.qth.flow.history, params);
if (code == 200) {
setHistoryList(data);
setLoading(false);
}
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
const getHistoryMax = async (params) => {
try {
const { data, code } = await httppost(apiurl.sq.qth.flow.max, params);
if (code == 200) {
setMaxInfo(data);
}
} catch (error) {
console.log(error);
}
};
const handleSearch = () => {
if (!stcd) return;
const params = {
stcd,
dateTimeRangeSo: {
start:dates && dates[0] ? dates[0].format('YYYY-MM-DD HH:mm:ss'):undefined,
end:dates && dates[1] ?dates[1].format('YYYY-MM-DD HH:mm:ss'):undefined
}
};
getHistoryList(params);
getHistoryMax(params)
};
useEffect(() => {
if (stcd) {
handleSearch();
}
}, [stcd]);
const option = useMemo(() => flowOption({ data: historyList }), [historyList]);
return (
<div className="right-panel">
<div className="panel-header" >
<div className="query-label"><span className="dot"></span></div>
<div className="query-controls" style={cleanMode ? { marginLeft: 0 } : {}}>
<RangePicker
showTime
value={dates}
onChange={setDates}
style={{ width: 340 }}
format="YYYY-MM-DD HH:mm"
allowClear={false}
/>
<Button type="primary" className="ant-btn-ghost-blue" icon={<SearchOutlined />} onClick={handleSearch} loading={loading}>
查询
</Button>
</div>
</div>
<div className="table-chart-layout">
<div className="data-table">
<Table
columns={columns}
dataSource={historyList}
size="small"
pagination={false}
scroll={{ y: 400 }}
rowKey="tm"
/>
</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">
<div className="grid-header">最大瞬时流量(/s)</div>
<div className="grid-header">累计水量(万m³)</div>
<div className="grid-value">
{maxInfo?.maxQ ?? '-'}
{maxInfo?.maxQTm &&<span style={{fontSize: 12, color: '#aaa', marginLeft: 8}}>({moment(maxInfo?.maxQTm).format('YYYY-MM-DD HH:mm:ss')})</span>}
</div>
<div className="grid-value">{maxInfo?.totalWater ?? '-'}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,76 @@
import moment from "moment";
export default function flowOption({ data }) {
const qList = data.map(o => o.q ?? 0);
const wList = data.map(o => o.totalWater ?? 0);
const tms = data?.map(o=> o.tm ? moment(o.tm).format('YYYY-MM-DD HH:mm'):null)
const maxQ = Math.ceil(Math.max(...qList, 0))
const minQ = Math.floor(Math.min(...qList, 0))
const maxW = Math.ceil(Math.max(...wList, 0))
const minW = Math.floor(Math.min(...wList, 0))
return {
tooltip: { trigger: 'axis' },
legend: {
show: true,
textStyle: { color: '#fff' },
data: ['累计水量', '瞬时流量']
},
grid: {
top: '15%', left: '2%', right: '2%', bottom: '5%', containLabel: true
},
xAxis: {
type: 'category',
data: tms,
inverse:true,
axisLabel: { color: '#fff' },
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } }
},
yAxis: [
{
type: 'value',
name: '累计水量(万m³)',
position: 'left',
axisLabel: { color: '#fff' },
nameTextStyle: { color: '#fff' },
splitLine: { show: false },
min: minW,
max:maxW
},
{
type: 'value',
name: '瞬时流量(m³/s)',
position: 'right',
axisLabel: { color: '#fff' },
nameTextStyle: { color: '#fff' },
splitLine: {
show: true,
lineStyle: { color: 'rgba(255,255,255,0.2)', type: 'dashed' }
},
min: minQ,
max:maxQ
}
],
series: [
{
name: '累计水量',
type: 'line',
yAxisIndex: 0,
data: wList,
itemStyle: { color: '#1890ff' }, // Blue
showSymbol: false,
smooth: true
},
{
name: '瞬时流量',
type: 'line',
yAxisIndex: 1,
data: qList,
itemStyle: { color: '#52c41a' }, // Green
showSymbol: false,
smooth: true
}
]
};
}

View File

@ -0,0 +1,115 @@
import React, { useEffect, useState } from 'react';
import { Select } from 'antd';
import { flowlist } from '@/service/station';
import RightPanel from './RightPanel';
import moment from 'moment';
import './index.less';
const FlowPanel = ({ stcd, cleanMode = false }) => {
const [selectList, setSelectList] = useState([]);
const [selected, setSelected] = useState('');
const [detail, setDetail] = useState({});
// 获取站点
const getStationList = async () => {
try {
// Reusing reservoir list for now
const data = await flowlist({});
const formattedData = data.map(item => ({
label: item.stnm,
value: item.stcd,
...item
}));
setSelectList(formattedData);
let targetStcd = selected || stcd;
if (targetStcd) {
const item = formattedData.find(i => i.stcd === targetStcd);
if (item) {
setSelected(targetStcd);
setDetail(item);
} else if (formattedData.length > 0) {
setSelected(formattedData[0].stcd);
setDetail(formattedData[0]);
}
} else if (formattedData.length > 0) {
setSelected(formattedData[0].stcd);
setDetail(formattedData[0]);
}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
if (stcd) {
setSelected(stcd);
}
}, [stcd]);
const handleStationChange = (val) => {
setSelected(val);
const item = selectList.find(i => i.value === val);
if (item) {
setDetail(item);
}
};
useEffect(() => {
getStationList();
}, []);
const {
q,change24Q,avg24Q,total24V
} = detail || {}
const stats = [
{ label: '站点类型', value: '-', unit: '' },
{ label: '实时瞬时流量', value: q ?? '-', unit: 'm³/s' },
{ label: '近24h变幅', value: change24Q ?? '-', unit: 'm³/s' },
{ label: '近24h平均流量', value: avg24Q ?? '-', unit: 'm³/s' },
{ label: '近24h累计水量', value: total24V ?? '-', unit: '万m³' },
];
return (
<div className="flow-panel-container">
<div className="main-content">
<div className="left-panel">
<div className="panel-header">
<div className="query-label"><span className="dot"></span></div>
{!cleanMode && (
<div className="station-select">
<span>站点</span>
<Select
value={selected}
onChange={handleStationChange}
style={{ width: 200 }}
options={selectList}
/>
</div>
)}
</div>
<div className="update-time-banner">
流量最新上报时间{detail?.tm ?? moment().format('YYYY-MM-DD HH:mm:ss')}
</div>
<div className="data-list">
{stats.map((item, idx) => (
<div key={idx} className="data-item">
<div className="label">{item.label}:</div>
<div className="value-container">
<span className="value">{item.value}</span>
<span className="unit">{item.unit}</span>
</div>
</div>
))}
</div>
</div>
<RightPanel stcd={selected} cleanMode={cleanMode} />
</div>
</div>
);
};
export default FlowPanel;

View File

@ -0,0 +1,199 @@
.flow-panel-container {
height: 100%;
display: flex;
flex-direction: column;
color: #fff;
.main-content {
flex: 1;
display: flex;
gap: 16px;
overflow: hidden;
.left-panel {
width: 440px;
display: flex;
flex-direction: column;
gap: 15px;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
.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;
gap: 8px;
}
}
.update-time-banner {
width: 70%;
margin: 0 auto;
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;
margin-bottom: 0px;
}
.data-list {
display: flex;
flex-direction: column;
gap: 5px;
padding: 0 10px;
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 8px;
border-bottom: 1px dashed rgba(255, 255, 255, 0.2);
.label {
font-size: 14px;
color: rgba(255, 255, 255);
}
.value-container {
display: flex;
align-items: center;
gap: 4px;
.value {
font-size: 16px;
font-weight: bold;
font-family: Arial, sans-serif;
}
.unit {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
margin-left: 4px;
}
}
}
}
.visual-placeholder {
flex: 1;
background: rgba(255, 255, 255, 0.02);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.3);
border: 1px dashed rgba(255, 255, 255, 0.1);
}
}
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
border-radius: 4px;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
.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: 10px;
}
}
.table-chart-layout {
flex: 1;
display: flex;
gap: 16px;
overflow: hidden;
.data-table {
width: 450px;
overflow: auto;
}
.chart-view {
flex: 1;
display: flex;
flex-direction: column;
.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: 50%;
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: 50%;
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;
}
}
}
}
}
}

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,176 @@
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: 'rgba(255,255,255,0.5)',
}
},
axisTick: {
show: false,
},
}
],
yAxis: [
{
type: 'value',
position: 'left',
name: "雨量mm",
nameTextStyle: { color: '#fff' },
splitLine: {
show: true,
lineStyle: {
color: 'rgba(255,255,255,0.5)',
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: 'rgba(255,255,255,0.5)',
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

@ -0,0 +1,174 @@
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,record }) {
const defaultRange = [
moment().subtract(7, 'days').set({ minute: 0, second: 0 }),
moment().set({ minute: 0, second: 0 }),
];
const [dates, setDates] = useState(defaultRange);
const [historyTableList, setHistoryTableList] = useState([]);
const [loading, setLoading] = useState(false);
const [historySkDetail, setHistorySkDetail] = useState({});
const columns = [
{
title: '时间', dataIndex: 'time', key: 'time', align: 'center', width: 120,
render:(rec,row)=>row.tm?moment(row.tm).format('MM-DD HH:mm'):''
},
{
title: '雨量(mm)', dataIndex: 'drp', key: 'drp', align: 'center',
render: (rec) => <span>{rec ?? "-"}</span>
},
{
title: '水位(m)', dataIndex: 'rz', key: 'rz', align: 'center',
render: (rec) => <span>{rec ? rec.toFixed(2) : "-"}</span>
},
{
title: '库容(万m³)', dataIndex: 'w', key: 'w', align: 'center',
render: (rec) => <span>{rec ??"-"}</span>,
},
];
// Placeholder stats
const {
h1, h3, h6, h12, h24, h48,
yearDrpDay, rzDiff, today, yesterdayDrp,
monthDrp, yearDrp, maxDrp, maxDrpTime, maxRz
} = historySkDetail || {};
const bottomStats = [
{ label: '近1h雨量(mm)', value: h1 ?? '-' },
{ label: '近3h雨量(mm)', value: h3 ?? '-' },
{ label: '近6h雨量(mm)', value: h6 ?? '-' },
{ label: '近12h雨量(mm)', value: h12 ?? '-' },
{ label: '近24h雨量(mm)', value: h24 ?? '-' },
{ label: '本年降雨天数', value: yearDrpDay || 0 },
{ label: '24h水位变幅(m)', value: rzDiff ? (rzDiff > 0 ? "+" : "") + rzDiff.toFixed(2) : '-' },
{ label: '近48h雨量(mm)', value: h48 ?? '-' },
{ label: '今日雨量(mm)', value: today ?? '-' },
{ label: '昨日雨量(mm)', value: yesterdayDrp ?? '-' },
{ label: '本月雨量(mm)', value: monthDrp ?? '-' },
{ label: '本年雨量(mm)', value: yearDrp ?? '-' },
{ label: '本年最大日雨量(mm)', value: maxDrp !==null ? `${maxDrp}(${maxDrpTime ? moment(maxDrpTime).format('MM-DD') : ''})` : '-' },
{ label: '本年最高水位(m)', value: maxRz ? maxRz.toFixed(2) : '-' },
];
// Chart Option
const option = useMemo(() => {
return drpOption({data:historyTableList,afsltdz:record.afsltdz,flLowLimLev:record.flLowLimLev,desFloodLev:record.desFloodLev,calFloodLev:record.calFloodLev})
}, [historyTableList]);
// 获取水库历史数据
const getHistoryList = async (params) => {
setLoading(true);
try {
const { data, code } = await httppost(apiurl.sq.qth.reservoir.monitor, params)
if (code == 200) {
setLoading(false)
setHistoryTableList(data)
}
} catch (error) {
console.log(error);
}
}
// 获取水库近几小时数据
const getHourDetail= async (params) => {
try {
const { data, code } = await httpget(apiurl.sq.qth.reservoir.detail, params)
if (code == 200) {
setHistorySkDetail(data);
}
} catch (error) {
console.log(error);
}
}
const handleSearch = async () => {
if (!stcd) return;
const params = {
stcd,
stm: dates[0].format('YYYY-MM-DD HH:mm:ss'),
etm: dates[1].format('YYYY-MM-DD HH:mm:ss')
};
getHistoryList(params)
getHourDetail(params)
};
useEffect(() => {
if (stcd) {
handleSearch();
}
}, [stcd]);
return (
<div className="right-panel">
<div className="panel-header" >
<div className="query-label"><span className="dot"></span></div>
<div className="query-controls" style={cleanMode ? { marginLeft: 0 } : {}}>
<RangePicker
showTime
value={dates}
onChange={setDates}
style={{ width: 340 }}
format="YYYY-MM-DD HH:mm"
allowClear={false}
/>
<Button type="primary" className="ant-btn-ghost-blue" icon={<SearchOutlined />} onClick={handleSearch} loading={loading}>
查询
</Button>
</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, 7).map((i, idx) => <div className="grid-header" key={`h1-${idx}`}>{i.label}</div>)}
{bottomStats.slice(0, 7).map((i, idx) => (
<div className="grid-value" key={`v1-${idx}`}>
<span className={i.suffix ? 'special-text' : ''}>{i.value}</span>
</div>
))}
{bottomStats.slice(7, 14).map((i, idx) => <div className="grid-header" key={`h2-${idx}`}>{i.label}</div>)}
{bottomStats.slice(7, 14).map((i, idx) => (
<div className="grid-value" key={`v2-${idx}`}>
{i.value}
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,257 @@
export default function drpOption ({
data,
afsltdz,
flLowLimLev,
desFloodLev,
calFloodLev
}) {
// flLowLimLev 汛限水位 desFloodLev 设计水位 calFloodLev校核水位
const maxVal = Math.max(...data.map(obj => obj.drp))
// const minVal = Math.min(...data.map(obj => obj.drp))
const maxSw = Math.ceil(Math.max(...data.map(obj => obj.rz),flLowLimLev,desFloodLev,calFloodLev))
const minSw = Math.floor(Math.min(...data.map(obj => obj.rz),flLowLimLev,desFloodLev,calFloodLev))
const maxKr = Math.ceil(Math.max(...data.map(obj => obj.w)))
const minKr = Math.floor(Math.min(...data.map(obj => obj.w)))
return {
tooltip: {
trigger: 'axis'
},
grid: [
{
top: '15%',
left: '10%',
right: '8%',
width: '80%',
height: '35%'
},
{
bottom: '5%',
left: '10%',
right: '8%',
width: '80%',
height: '35%'
}
],
legend: {
// 显示图例
show: true,
textStyle: { color: '#fff' },
// 图例的位置
// data: ['汛限水位', '设计水位', '校核水位', "降雨量", "水位", "库容"],
data: ['校核水位', '设计水位', '汛限水位', '降雨量', '水位', '库容']
},
xAxis: [
{
gridIndex: 0,
type: 'category',
data: data.map(o => o.tm).reverse(),
splitLine: {
show: false
},
axisLabel: {
color: '#fff',
fontSize: 12,
show: false
},
axisLine: {
lineStyle: {
color: 'rgba(255,255,255,0.5)',
}
},
axisTick: {
show: false
}
},
{
gridIndex: 1,
type: 'category',
data: data.map(o => o.tm),
inverse: true,
splitLine: {
show: false
},
axisLabel: {
color: '#fff',
fontSize: 12,
formatter: val => val.substr('2020-'.length, 11)
},
axisLine: {
lineStyle: {
color: 'rgba(255,255,255,0.5)',
}
},
axisTick: {
show: false
}
}
],
yAxis: [
{
inverse: true,
gridIndex: 0,
type: 'value',
position: 'left',
name: '降雨量(mm)',
nameLocation: 'start',
nameTextStyle: {
color: '#fff'
},
axisLabel: {
color: '#fff',
fontSize: 12
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(255,255,255,0.5)',
type: 'dotted'
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
min: 0,
max: maxVal
},
{
gridIndex: 1,
type: 'value',
position: 'left',
name: '水位(m)',
nameTextStyle: {
color: '#fff'
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(255,255,255,0.5)',
type: 'dotted'
}
},
axisLabel: {
color: '#fff',
fontSize: 12
},
axisLine: {
show: false
},
axisTick: {
show: false
},
min: minSw,
max: maxSw
},
{
gridIndex: 1,
type: 'value',
position: 'right',
name: '库容(万m³)',
nameTextStyle: {
color: '#fff'
},
splitLine: {
show: false,
lineStyle: {
color: '#07a6ff',
width: 0.25,
type: 'dotted'
}
},
axisLabel: {
color: '#fff',
fontSize: 12
},
axisLine: {
show: false
},
axisTick: {
show: false
},
min: minKr,
max: maxKr
}
],
series: [
{
xAxisIndex: 1,
yAxisIndex: 1,
name: '校核水位',
type: 'line',
color: '#D9001B',
lineStyle: {
type: 'dashed'
},
data: data.map(o => calFloodLev),
symbol: 'none' // 设置标记点为'none',即去掉圆点
},
{
xAxisIndex: 1,
yAxisIndex: 1,
name: '设计水位',
type: 'line',
color: '#F59A23',
data: data.map(o => desFloodLev),
lineStyle: {
type: 'dashed'
},
symbol: 'none' // 设置标记点为'none',即去掉圆点
},
{
xAxisIndex: 1,
yAxisIndex: 1,
name: '汛限水位',
type: 'line',
color: '#FDDC9F',
data: data.map(o => {
return flLowLimLev
}),
lineStyle: {
type: 'dashed'
},
symbol: 'none' // 设置标记点为'none',即去掉圆点
},
{
name: '降雨量',
type: 'bar',
barWidth: '60%',
data: data.map(o => o.drp).reverse(),
itemStyle: {
color: '#007AFD'
},
label: {
show: false
}
},
{
xAxisIndex: 1,
yAxisIndex: 1,
name: '水位',
type: 'line',
symbol: 'none',
color: '#0AE0B5',
label: {
show: false
},
data: data.map(o => o.rz ? o.rz.toFixed(2):null )
},
{
xAxisIndex: 1,
yAxisIndex: 2,
name: '库容',
type: 'line',
color: '#007AFD',
symbol: 'none',
showSymbol: false,
label: {
show: false
},
data: data.map(o => o.w)
}
]
}
}

View File

@ -0,0 +1,133 @@
import React, { useEffect, useState } from 'react';
import { Select } from 'antd';
import apiurl from '@/service/apiurl';
import { httpget, httppost } from '@/utils/request';
import { reservoirlist } from '@/service/station';
import RightPanel from './RightPanel';
import moment from 'moment';
import MyImg from './myImg';
import './index.less';
const ReservoirPanel = ({ stcd, cleanMode = false }) => {
const [selectList, setSelectList] = useState([]);
const [selected, setSelected] = useState('');
const [reservoirDetail, setReservoirDetail] = useState({});
// 获取水库站点
const getReservoirList = async () => {
try {
const data = await reservoirlist({});
const formattedData = data.map(item => ({
label: item.stnm,
value: item.stcd,
...item
}));
setSelectList(formattedData);
// Determine which station to select
let targetStcd = selected || stcd;
if (targetStcd) {
const item = formattedData.find(i => i.stcd === targetStcd);
if (item) {
setSelected(targetStcd);
setReservoirDetail(item);
} else if (formattedData.length > 0) {
// Fallback if target not found
setSelected(formattedData[0].stcd);
setReservoirDetail(formattedData[0]);
}
} else if (formattedData.length > 0) {
setSelected(formattedData[0].stcd);
setReservoirDetail(formattedData[0]);
}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
if (stcd) {
setSelected(stcd);
}
}, [stcd]);
const handleStationChange = (val) => {
setSelected(val);
const item = selectList.find(i => i.value === val);
if (item) {
setReservoirDetail(item);
}
};
useEffect(() => {
getReservoirList();
}, []);
// Data items for the left list
const dataItems = [
{ label: '监测水位', value: reservoirDetail?.rz ??'-', unit: 'm', isPrimary: true },
{ label: '校核洪水位', value: reservoirDetail?.calFloodLev ?? '-', unit: 'm', diff: (reservoirDetail?.calFloodLev && reservoirDetail?.rz) ? (reservoirDetail.rz - reservoirDetail.calFloodLev).toFixed(2) : null },
{ label: '设计洪水位', value: reservoirDetail?.desFloodLev ?? '-', unit: 'm', diff: (reservoirDetail?.desFloodLev && reservoirDetail?.rz) ? (reservoirDetail.rz - reservoirDetail.desFloodLev).toFixed(2) : null },
{ label: '汛限水位', value: reservoirDetail?.flLowLimLev ?? '-', unit: 'm', diff: (reservoirDetail?.flLowLimLev && reservoirDetail?.rz) ? (reservoirDetail.rz - reservoirDetail.flLowLimLev).toFixed(2) : null },
{ label: '死水位', value: reservoirDetail?.deadLev ?? '-', unit: 'm', diff: (reservoirDetail?.deadLev && reservoirDetail?.rz) ? (reservoirDetail.rz - reservoirDetail.deadLev ).toFixed(2) : null },
{ label: '坝顶高程', value: reservoirDetail?.crestElev ?? '-', unit: 'm' },
{ label: '水库当前库容', value: reservoirDetail?.nowCap ?? '-', unit: '万m³' },
{ label: '兴利库容', value: reservoirDetail?.benResCap ?? '-', unit: '万m³' },
];
return (
<div className="reservoir-panel-container">
<div className="main-content">
{/* Left Side: Stats List */}
<div className="left-panel">
<div className="panel-header">
<div className="query-label"><span className="dot"></span></div>
{!cleanMode && (
<div className="station-select">
<span>站点</span>
<Select
value={selected}
onChange={handleStationChange}
style={{ width: 200 }}
options={selectList}
/>
</div>
)}
</div>
<div className="update-time-banner">
水位上报时间{reservoirDetail?.tm ?? moment().format('YYYY-MM-DD HH:mm:ss')}
</div>
<div className="data-list">
{dataItems.map((item, idx) => (
<div key={idx} className="data-item">
<div className="label">{item.label}:</div>
<div className="value-container">
<span className="value">{item.value}</span>
{item.diff && (
<span className="diff" style={{ color: item.diff > 0 ? '#ff4d4f' : '#52c41a' }}>
{item.diff > 0 ? `| +${item.diff}` : `| -${Math.abs(item.diff)}`}
</span>
)}
<span className="unit">{item.unit}</span>
</div>
</div>
))}
</div>
<div className="visual-placeholder">
<div className="placeholder-content">
<MyImg record={reservoirDetail}/>
</div>
</div>
</div>
<RightPanel stcd={selected} record={reservoirDetail} cleanMode={cleanMode} />
</div>
</div>
);
};
export default ReservoirPanel;

View File

@ -0,0 +1,262 @@
.reservoir-panel-container {
height: 100%;
display: flex;
flex-direction: column;
color: #fff;
.main-content {
flex: 1;
display: flex;
gap: 16px;
overflow: hidden;
.left-panel {
width: 480px;
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 {
width: 70%;
margin: 0 auto;
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;
margin-bottom: 0px;
}
.data-list {
display: flex;
flex-direction: column;
gap: 5px;
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 2px;
border-bottom: 1px dashed rgba(255, 255, 255, 0.2);
.label {
color: #fff;
font-size: 14px;
}
.value-container {
display: flex;
align-items: center;
gap: 10px;
.value {
font-size: 16px;
font-weight: bold;
color: #fff;
}
.diff {
font-size: 14px;
font-weight: bold;
}
.unit {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
margin-left: 4px;
}
}
}
}
.visual-placeholder {
flex: 1;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
margin-top: 10px;
overflow: hidden; /* Added to contain overflow */
.placeholder-content {
width: 100%;
height: 100%;
position: relative;
/* text-align: center; removed */
/* color: rgba(255, 255, 255, 0.5); removed */
/* img { removed generic img style as MyImg handles it
width: 100%;
height: 100%;
object-fit: contain;
} */
}
}
}
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
// padding: 16px;
border-radius: 4px;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
.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: 10px;
align-items: center;
.ant-picker {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(0, 160, 233, 0.5);
color: #fff;
input { color: #fff; }
.ant-picker-suffix { color: rgba(255, 255, 255, 0.5); }
}
}
}
.table-chart-layout {
flex: 1;
display: flex;
gap: 16px;
overflow: hidden;
.data-table {
flex: 0 0 400px;
.ant-table {
background: transparent;
color: #fff;
.ant-table-thead > tr > th {
background: rgba(0, 160, 233, 0.2);
color: #fff;
border-bottom: none;
}
.ant-table-tbody > tr > td {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
&:hover {
background: rgba(0, 160, 233, 0.1) !important;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 160, 233, 0.1) !important;
}
}
}
.chart-view {
flex: 1;
display: flex;
flex-direction: column;
.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: 14.2%;
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: 14.2%;
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;
}
}
}
}
}
}
.ant-btn-ghost-blue {
background: transparent;
border-color: #00a0e9;
color: #00a0e9;
&:hover, &:focus {
color: #40a9ff;
border-color: #40a9ff;
background: rgba(0, 160, 233, 0.1);
}
}

View File

@ -0,0 +1,154 @@
const MyImg = ({ record }) => {
console.log("record: ", record);
/**
*
* @param {*} h1 动态水位
* @param {*} h2 坝高
* @param {*} h3 死水位
* @returns
*/
const computerHeight = (h1, h2, h3) => {
let height;
let comHeight;
if (h1 && h2 && h3) {
comHeight = (h1 - h3) / (h2 - h3)
// height = comHeight * 100 < 80 ? `${comHeight}%` : "80%"
height = `${comHeight * 80}%`
return comHeight > 0 ? height : 10
} else {
return '0%'
}
}
return (
<div style={{
width: '100%',
height: '100%',
overflow: 'hidden',
position: 'relative',
backgroundColor: "#eff3f6",
borderRadius: "2%"
}}>
<img
src={`${process.env.PUBLIC_URL}/assets/images/waterz.png`}
style={
{
position: 'absolute',
bottom: computerHeight(record.rz, record.crestElev, record.deadLev),
// top: '50%'
transform: "translateY(100%)"
}}
/>
<img src={`${process.env.PUBLIC_URL}/assets/images/dam.png`} style={{ position: 'absolute', bottom: 0, height: '100%', right: -20 }} />
<img src={`${process.env.PUBLIC_URL}/assets/images/ruler.png`} style={{ position: 'absolute', bottom: 0, height: '100%', left: 20 }} />
<div style={
{
position: 'absolute',
left: 40,
// width: '65%',
bottom: computerHeight(record.desFloodLev, record.crestElev, record.deadLev),
display: 'flex',
alignItems: 'center',
gap: 8,
// zIndex:1
}}>
<img src={`${process.env.PUBLIC_URL}/assets/images/red1.png`}
style={{
width: 90,
height: 15,
}}
/>
<div style={{
color: '#ff0a0a',
fontSize: 16,
justifyContent: 'space-between',
display: 'flex',
columnGap: 10
}}>
设计洪水位 <strong>{record.desFloodLev ? record.desFloodLev.toFixed(2) : "-"}m</strong>
</div>
</div>
<div style={
{
position: 'absolute',
left: 40,
// width: '65%',
bottom: computerHeight(record.flLowLimLev, record.crestElev, record.deadLev),
display: 'flex',
alignItems: 'center',
gap: 8
}}>
<img src={`${process.env.PUBLIC_URL}/assets/images/orange.png`}
style={{
width: 90,
height: 15
}}
/>
<div style={{
// width: "210px",
// border: '2px solid red',
// backgroundColor: '#0008',
// padding: '2px 16px',
color: '#ff7200',
fontSize: 16,
justifyContent: 'space-between',
display: 'flex',
columnGap: 10
}}>
汛限水位 <strong>{record.flLowLimLev ? record.flLowLimLev.toFixed(2) : "-"}m</strong>
</div>
</div>
<div style={
{
position: 'absolute',
width: '84%',
bottom: computerHeight(record.rz, record.crestElev, record.deadLev),
display: 'flex',
alignItems: 'center',
gap: 8,
justifyContent: 'end',
}}>
<img src={`${process.env.PUBLIC_URL}/assets/images/blue2.png`}
style={{
flex: 0.5,
// width: 30,
height: 15
}}
/>
<div style={{color: '#06caff',fontSize: 16}}>
<div style={{ display: 'flex', columnGap: 10 }}>
<span>实时水位</span> <strong>{record.rz ? record.rz.toFixed(2) : "-"}m</strong>
</div>
</div>
</div>
<div style={{
position: 'absolute', left: 40,
// width: '65%',
top: '90%', transform: 'translateY(-50%)', display: 'flex', alignItems: 'center', gap: 8
}}>
<img src={`${process.env.PUBLIC_URL}/assets/images/white1.png`}
style={{
width: 90,
height: 15
}}
/>
<div style={{
color: '#fff',
fontSize: 16,
justifyContent: 'space-between',
display: 'flex',
columnGap: 10
}}>
死水位 <strong>{record.deadLev ? record.deadLev.toFixed(2) : ''}m</strong>
</div>
</div>
</div>
)
}
export default MyImg

View File

@ -0,0 +1,26 @@
import React, { useEffect, useMemo, useState } from 'react';
import { DatePicker, Button, Table } from 'antd';
import ReactEcharts from 'echarts-for-react';
import usePageTable from '@/components/crud/usePageTable';
import { createCrudService } from '@/components/crud/_';
import apiurl from '@/service/apiurl';
import './index.less';
import RainMonitor from './RainMonitor';
import ReservoirPanel from './ReservoirPanel';
import FlowPanel from './FlowPanel';
const { RangePicker } = DatePicker;
const SafetyPanel = () => {
return (
<div className="awm-empty">内容待接入</div>
);
};
const AllWeatherModal = ({ active }) => {
if (active === 'rain') return <RainMonitor />;
if (active === 'reservoir') return <ReservoirPanel />;
if (active === 'flow') return <FlowPanel />;
return <SafetyPanel />;
};
export default AllWeatherModal;

View File

@ -0,0 +1,32 @@
.all-weather-modal {
height: 100%;
display: flex;
flex-direction: column;
}
.awm-grid {
height: calc(100% - 48px);
display: flex;
gap: 16px;
}
.awm-left {
flex: 0 0 420px;
display: flex;
flex-direction: column;
gap: 10px;
}
.awm-toolbar {
display: flex;
align-items: center;
gap: 10px;
}
.awm-right {
flex: 1;
min-width: 0;
}
.awm-empty {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255,255,255,0.6);
}

View File

@ -0,0 +1,187 @@
import React, { useState, useEffect } from 'react';
import { Timeline, Select, DatePicker, Button, Tag, Image, Tooltip } from 'antd';
import { SearchOutlined, ReloadOutlined, DownloadOutlined, FilePdfOutlined, FileOutlined } from '@ant-design/icons';
import apiurl from '@/service/apiurl';
import usePageTable from '@/components/crud/usePageTable';
import { createCrudService } from '@/components/crud/_';
import { exportFile } from '@/utils/tools'
import { httppost } from '@/utils/request';
import { config } from '@/config';
import PdfView from '@/views/Home/components/UI/PdfView';
import './index.less';
const { RangePicker } = DatePicker;
const CycleArchive = () => {
const { tableProps, search, refresh } = usePageTable(createCrudService(apiurl.sq.qzq.list).find);
const [keyword, setKeyword] = useState([]);
const [dates, setDates] = useState();
const [pdfInfo, setPdfInfo] = useState({ visible: false, title: '', fileId: '' });
useEffect(() => {
search({ search: {} });
}, []);
const handleSearch = () => {
const params = {
search: {
types: keyword.length > 0 ? keyword : undefined,
dateSo: dates ? {
start: dates[0]?.format('YYYY-MM-DD'),
end: dates[1]?.format('YYYY-MM-DD')
} : undefined
}
};
search(params);
};
const handleReset = () => {
setKeyword([]);
setDates(undefined);
search({ search: {} });
};
const onExport = () => {
let params = {
search: {
types: keyword.length > 0 ? keyword : undefined,
dateSo: dates ? {
start: dates[0]?.format('YYYY-MM-DD'),
end: dates[1]?.format('YYYY-MM-DD')
} : undefined
},
pageSo:{
pageNum:1,pageSize:9999
}
}
httppost(apiurl.sq.qzq.export, params,'blob').then(res => {
exportFile(`全周期档案.xlsx`,res.data)
})
}
const handleFileClick = (file) => {
const fileType = file.fileName?.split('.').pop()?.toLowerCase();
if (fileType === 'pdf') {
setPdfInfo({
visible: true,
title: file.fileName,
fileId: file.fileId
});
} else {
// Download for non-pdf files
window.open(config.minioIp + file.filePath, '_blank');
}
};
const renderFileIcon = (file) => {
const fileType = file.fileName?.split('.').pop()?.toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(fileType);
if (isImage) {
return (
<Image
width={60}
height={60}
src={config.minioIp + file.filePath}
style={{ objectFit: 'cover', borderRadius: '4px' }}
/>
);
} else if (fileType === 'pdf') {
return <FilePdfOutlined style={{ fontSize: '40px', color: '#ff4d4f' }} />;
} else {
return <FileOutlined style={{ fontSize: '40px', color: '#1890ff' }} />;
}
};
return (
<div className="cycle-archive-container">
{/* Filters */}
<div className="filter-bar">
<div className="filter-item">
<span className="label">类型</span>
<Select
options={[
{value:1,label:'大事记'}, {value:2,label:'调度指令'}, {value:3,label:'维修养护'},{value:4,label:'安全鉴定'}, {value:5,label:"除险加固"}
]}
allowClear
placeholder="请输入类型"
style={{ width: 200 }}
value={keyword}
onChange={(e) => setKeyword(e)}
mode='multiple'
maxTagCount='responsive'
/>
</div>
<div className="filter-item">
<span className="label">发生日期</span>
<RangePicker
style={{ width: 300 }}
value={dates}
onChange={(val) => setDates(val)}
/>
</div>
<div className="action-buttons">
<Button type="primary" className="ant-btn-ghost-blue" icon={<SearchOutlined />} onClick={handleSearch}>查询</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
<Button type="primary" icon={<DownloadOutlined />} className="ant-btn-ghost-blue" onClick={onExport}>导出</Button>
</div>
</div>
{/* Timeline Content */}
<div className="timeline-content">
<Timeline>
{tableProps.dataSource?.map((item, index) => (
<Timeline.Item
key={index}
label={
<div className="timeline-label-content">
<div className="date">{item.eventsDate}</div>
<Tag color="cyan" className="custom-tag">{item.typeName}</Tag>
</div>
}
>
<div className="timeline-item-content">
<div className="item-header">
<span className="title">{item.eventsDesc}</span>
</div>
<div className="item-body">
<div className="attachment-label">附件</div>
<div className="image-grid">
{item.files?.length > 0 && item.files.map((file, idx) => (
<div
key={idx}
className="image-item"
onClick={() => !['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(file.fileName?.split('.').pop()?.toLowerCase()) && handleFileClick(file)}
style={{ cursor: 'pointer' }}
>
<Tooltip title={file.fileName}>
<div className="file-preview">
{renderFileIcon(file)}
</div>
</Tooltip>
<span className="image-name">{file.fileName}</span>
</div>
))}
</div>
</div>
</div>
</Timeline.Item>
))}
</Timeline>
</div>
{pdfInfo.visible && (
<PdfView
visible={pdfInfo.visible}
onClose={() => setPdfInfo({ ...pdfInfo, visible: false })}
title={pdfInfo.title}
fileId={pdfInfo.fileId}
url={"/gunshiApp/ss/projectEvents/file/download/"}
/>
)}
</div>
);
};
export default CycleArchive;

View File

@ -0,0 +1,163 @@
.cycle-archive-container {
padding: 0px;
height: 100%;
display: flex;
flex-direction: column;
.filter-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
padding: 10px;
border-radius: 4px;
.filter-item {
display: flex;
align-items: center;
.label {
color: #fff;
margin-right: 10px;
white-space: nowrap;
}
}
.action-buttons {
display: flex;
gap: 10px;
}
}
.timeline-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden; // Prevent horizontal scrollbar
padding: 20px;
.ant-timeline-item {
padding-bottom: 20px;
}
.ant-timeline-item-label {
color: #00a0e9;
font-size: 16px;
width: 120px !important; // Fixed width for label
position: absolute !important;
left: 0 !important;
text-align: right;
padding-right: 15px;
top: 0;
}
.timeline-label-content {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 5px;
.date {
color: #fff;
font-size: 16px;
line-height: 1;
}
.custom-tag {
margin: 0;
border: none;
color: #fff;
background: #00a0e9; // Match theme
font-weight: normal;
text-align: center;
width: fit-content;
font-size: 14px;
padding: 0 5px;
border-radius: 2px;
}
}
.ant-timeline-item-tail {
border-left: 2px solid rgba(0, 160, 233, 0.3);
left: 126px !important; // 120px (label) + ~6px (center of gap)
height: calc(100% - 10px);
}
.ant-timeline-item-head {
background-color: transparent;
border-color: #00a0e9;
left: 126px !important; // Match tail
}
.ant-timeline-item-content {
margin-left: 140px !important; // 126px + padding
width: calc(100% - 140px) !important; // Take remaining width
top: 0;
padding-top: 0; // Align with label
min-height: auto;
}
.timeline-item-content {
background: transparent;
// border: 1px solid rgba(255, 255, 255, 0.1); // Removed border as per "clean" look
border-radius: 4px;
padding: 0;
width: 100%;
.item-header {
margin-bottom: 5px;
.title {
font-size: 16px;
color: #fff;
}
}
.item-body {
.attachment-label {
color: rgba(255, 255, 255, 0.6);
margin-bottom: 10px;
font-size: 14px;
}
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 15px;
.image-item {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.05);
padding: 8px;
border-radius: 4px;
gap: 10px;
width: 280px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s;
&:hover {
background: rgba(255, 255, 255, 0.1);
border-color: #00a0e9;
}
.file-preview {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
flex-shrink: 0;
}
.image-name {
color: rgba(255, 255, 255, 0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
}
}
}
}
}
}

View File

@ -0,0 +1,175 @@
import React, { useMemo } from 'react';
import ReactEcharts from 'echarts-for-react';
import { Table } from 'antd';
import { GetInterval } from '@/utils/tools';
const CapacityCurve = ({data=[]}) => {
const dataSource = (data && data.length > 0) ? data : [];
// Robust data parsing
const validData = dataSource.map(item => ({
...item,
rz: parseFloat(item.rz),
w: parseFloat(item.w)
})).filter(item => !isNaN(item.rz) && !isNaN(item.w));
const hasData = validData.length > 0;
let maxVal = hasData ? Math.ceil(Math.max(...validData.map(obj => obj.rz))) : 100;
let minVal = hasData ? Math.floor(Math.min(...validData.map(obj => obj.rz))) : 0;
let maxValX = hasData ? Math.max(...validData.map(obj => obj.w)) : 100;
let minValX = hasData ? Math.min(...validData.map(obj => obj.w)) : 0;
// Prevent min === max for axes
if (minVal === maxVal) {
maxVal += 1;
minVal -= 1;
}
if (minValX === maxValX) {
maxValX += 10;
minValX -= 10;
if (minValX < 0) minValX = 0;
}
// Calculate safe interval
let intervalX = GetInterval(minValX, maxValX);
if (intervalX <= 0 || isNaN(intervalX)) intervalX = 20;
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
render: (text, record, index) => index + 1,
width: 80,
align: 'center',
},
{
title: '水位(m)',
dataIndex: 'rz',
key: 'rz',
align: 'center',
},
{
title: '库容(万m³)',
dataIndex: 'w',
key: 'w',
align: 'center',
},
];
const option = useMemo(() => {
return {
toolbox: {
show: true,
feature: {
saveAsImage: {
show: true,
excludeComponents: ['toolbox'],
pixelRatio: 2,
name:"库容曲线图"
}
},
right: "14%",
top:"5%"
},
title: {
text: "库容曲线图",
left: "40%",
textStyle: {
color: '#fff',
}
},
tooltip: {
trigger: 'axis',
},
grid: [
{
top: "10%",
left: "15%",
right: "15%",
bottom: "8%"
},
],
xAxis: [
{
name: "库容(万m³)",
nameTextStyle: {
color: '#fff',
},
nameGap: 5,
type: 'value',
min:Math.floor(minValX / 5) *5,
max:Math.ceil(maxValX / 5) *5,
interval: intervalX,
splitLine: {
show: false
},
axisLabel: {
color: '#fff',
fontSize: 12,
},
}
],
yAxis: [
{
type: 'value',
name: "库水位(m)",
nameTextStyle: {
color: '#fff',
},
minInterval:1,
splitLine: {
show: true,
lineStyle: {
color: '#fff',
width: 0.25,
type: 'dotted'
}
},
axisLabel: {
color: '#fff',
fontSize: 12,
},
axisLine: {
show: false
},
axisTick: {
show: false,
},
min: minVal,
max: maxVal
}
],
series: [
{
type: 'line',
color: "#007AFD",
data: validData.map(item=>[item.w,item.rz]),
smooth: true
},
]
};
}, [minVal, maxVal, minValX, maxValX, intervalX, validData]);
return (
<div style={{ display: 'flex', height: '100%', gap: '20px' }}>
<div style={{ flex: '0 0 400px', height: '100%', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<Table
dataSource={dataSource}
columns={columns}
rowKey="id"
pagination={false}
scroll={{ y: 'calc(100vh - 300px)' }} // Estimate height, will adjust if needed
size="small"
bordered={false}
className="custom-table"
/>
</div>
<div style={{ flex: 1, height: '100%', minWidth: 0, paddingRight: 20 }}>
<ReactEcharts option={option} style={{ height: '100%', width: '100%' }} notMerge={true} lazyUpdate={true} />
</div>
</div>
);
};
export default CapacityCurve;

View File

@ -1,7 +1,8 @@
import React from 'react'; import React,{useState,useEffect} from 'react';
import { Descriptions } from 'antd'; import { Descriptions } from 'antd';
const MainBuildingInfo = ({data={}}) => {
const [info, setInfo] = useState({})
const MainBuildingInfo = ({ data = {} }) => {
const commonLabelStyle = { const commonLabelStyle = {
width: '200px', width: '200px',
textAlign: 'right', textAlign: 'right',
@ -21,63 +22,68 @@ const MainBuildingInfo = ({ data = {} }) => {
style: descriptionStyle, style: descriptionStyle,
}); });
useEffect(() => {
setInfo(data)
}, [data])
return ( return (
<div className="form-container"> <div className="form-container">
{/* 主坝 */} {/* 主坝 */}
<Descriptions {...getDescriptionsProps('主坝')}> <Descriptions {...getDescriptionsProps('主坝')}>
<Descriptions.Item label="坝型">{data.mainDamType || '-'}</Descriptions.Item> <Descriptions.Item label="坝型">{info.mainType || '-'}</Descriptions.Item>
<Descriptions.Item label="坝顶高程(m)">{data.mainDamCrestElevation || '-'}</Descriptions.Item> <Descriptions.Item label="坝顶高程(m)">{info.mainCrestElevation || '-'}</Descriptions.Item>
<Descriptions.Item label="坝顶长度(m)">{data.mainDamLength || '-'}</Descriptions.Item> <Descriptions.Item label="坝顶长度(m)">{info.mainCrestLength || '-'}</Descriptions.Item>
<Descriptions.Item label="坝顶宽度(m)">{data.mainDamWidth || '-'}</Descriptions.Item> <Descriptions.Item label="坝顶宽度(m)">{info.mainCrestWidth || '-'}</Descriptions.Item>
<Descriptions.Item label="最大坝高(m)">{data.mainDamMaxHeight || '-'}</Descriptions.Item> <Descriptions.Item label="最大坝高(m)">{info.mainMaxHeight || '-'}</Descriptions.Item>
</Descriptions> </Descriptions>
{/* 副坝 */} {/* 副坝 */}
<Descriptions {...getDescriptionsProps('副坝')}> <Descriptions {...getDescriptionsProps('副坝')}>
<Descriptions.Item label="坝型">{data.auxDamType || '-'}</Descriptions.Item> <Descriptions.Item label="坝型">{info.auxType || '-'}</Descriptions.Item>
<Descriptions.Item label="坝顶高程(m)">{data.auxDamCrestElevation || '-'}</Descriptions.Item> <Descriptions.Item label="坝顶高程(m)">{info.auxCrestElevation || '-'}</Descriptions.Item>
<Descriptions.Item label="坝顶长度(m)">{data.auxDamLength || '-'}</Descriptions.Item> <Descriptions.Item label="坝顶长度(m)">{info.auxCrestLength || '-'}</Descriptions.Item>
<Descriptions.Item label="坝顶宽度(m)">{data.auxDamWidth || '-'}</Descriptions.Item> <Descriptions.Item label="坝顶宽度(m)">{info.auxCrestWidth || '-'}</Descriptions.Item>
<Descriptions.Item label="最大坝高(m)">{data.auxDamMaxHeight || '-'}</Descriptions.Item> <Descriptions.Item label="最大坝高(m)">{info.auxMaxHeight || '-'}</Descriptions.Item>
</Descriptions> </Descriptions>
{/* 溢洪道 */} {/* 溢洪道 */}
<Descriptions {...getDescriptionsProps('溢洪道')}> <Descriptions {...getDescriptionsProps('溢洪道')}>
<Descriptions.Item label="型式">{data.spillwayType || '-'}</Descriptions.Item> <Descriptions.Item label="型式">{info.spillwayType || '-'}</Descriptions.Item>
<Descriptions.Item label="堰顶型式">{data.spillwayCrestType || '-'}</Descriptions.Item> <Descriptions.Item label="堰顶型式">{info.spillwayCrestType || '-'}</Descriptions.Item>
<Descriptions.Item label="地基特性">{data.spillwayFoundation || '-'}</Descriptions.Item> <Descriptions.Item label="地基特性">{info.spillwayFoundation || '-'}</Descriptions.Item>
<Descriptions.Item label="溢流堰顶高程(m)">{data.spillwayCrestElevation || '-'}</Descriptions.Item> <Descriptions.Item label="溢流堰顶高程(m)">{info.spillwayCrestElevation || '-'}</Descriptions.Item>
<Descriptions.Item label="溢流堰净宽(m)">{data.spillwayNetWidth || '-'}</Descriptions.Item> <Descriptions.Item label="溢流堰净宽(m)">{info.spillwayNetWidth || '-'}</Descriptions.Item>
<Descriptions.Item label="消能型式">{data.spillwayDissipationType || '-'}</Descriptions.Item> <Descriptions.Item label="消能型式">{info.spillwayEnergyDissipation || '-'}</Descriptions.Item>
<Descriptions.Item label="校核洪水下泄流量(m³/s)">{data.spillwayCheckFlow || '-'}</Descriptions.Item> <Descriptions.Item label="校核洪水下泄流量(m³/s)">{info.spillwayCheckFloodDischarge || '-'}</Descriptions.Item>
<Descriptions.Item label="设计洪水下泄流量(m³/s)">{data.spillwayDesignFlow || '-'}</Descriptions.Item> <Descriptions.Item label="设计洪水下泄流量(m³/s)">{info.spillwayDesignFloodDischarge || '-'}</Descriptions.Item>
<Descriptions.Item label="消能防冲下泄流量(m³/s)">{data.spillwayDissipationFlow || '-'}</Descriptions.Item> <Descriptions.Item label="消能防冲下泄流量(m³/s)">{info.spillwayScouringDischarge || '-'}</Descriptions.Item>
</Descriptions> </Descriptions>
{/* 灌溉发电洞 */} {/* 灌溉发电洞 */}
<Descriptions {...getDescriptionsProps('灌溉发电洞')}> <Descriptions {...getDescriptionsProps('灌溉发电洞')}>
<Descriptions.Item label="型式">{data.irrigationTunnelType || '-'}</Descriptions.Item> <Descriptions.Item label="型式">{info.irrigationType || '-'}</Descriptions.Item>
<Descriptions.Item label="衬砌型式">{data.irrigationTunnelLining || '-'}</Descriptions.Item> <Descriptions.Item label="衬砌型式">{info.irrigationLiningType || '-'}</Descriptions.Item>
<Descriptions.Item label="地基特性">{data.irrigationTunnelFoundation || '-'}</Descriptions.Item> <Descriptions.Item label="地基特性">{info.irrigationFoundation || '-'}</Descriptions.Item>
<Descriptions.Item label="进口底板高程(m)">{data.irrigationTunnelInletElevation || '-'}</Descriptions.Item> <Descriptions.Item label="进口底板高程(m)">{info.irrigationInletElevation || '-'}</Descriptions.Item>
<Descriptions.Item label="断面尺寸(m)">{data.irrigationTunnelSectionSize || '-'}</Descriptions.Item> <Descriptions.Item label="断面尺寸(m)">{info.irrigationCrossSection || '-'}</Descriptions.Item>
<Descriptions.Item label="洞长(m)">{data.irrigationTunnelLength || '-'}</Descriptions.Item> <Descriptions.Item label="洞长(m)">{info.irrigationLength || '-'}</Descriptions.Item>
<Descriptions.Item label="设计流量(m³/s)">{data.irrigationTunnelDesignFlow || '-'}</Descriptions.Item> <Descriptions.Item label="设计流量(m³/s)">{info.irrigationDesignFlow || '-'}</Descriptions.Item>
<Descriptions.Item label="进口闸门型式">{data.irrigationTunnelGateType || '-'}</Descriptions.Item> <Descriptions.Item label="进口闸门型式">{info.irrigationGateType || '-'}</Descriptions.Item>
<Descriptions.Item label="进口启闭机型式">{data.irrigationTunnelHoistType || '-'}</Descriptions.Item> <Descriptions.Item label="进口启闭机型式">{info.irrigationHoistType || '-'}</Descriptions.Item>
</Descriptions> </Descriptions>
{/* 放空洞 */} {/* 放空洞 */}
<Descriptions {...getDescriptionsProps('放空洞')}> <Descriptions {...getDescriptionsProps('放空洞')}>
<Descriptions.Item label="型式">{data.outletTunnelType || '-'}</Descriptions.Item> <Descriptions.Item label="型式">{info.emptyingType || '-'}</Descriptions.Item>
<Descriptions.Item label="衬砌型式">{data.outletTunnelLining || '-'}</Descriptions.Item> <Descriptions.Item label="衬砌型式">{info.emptyingLiningType || '-'}</Descriptions.Item>
<Descriptions.Item label="地基特性">{data.outletTunnelFoundation || '-'}</Descriptions.Item> <Descriptions.Item label="地基特性">{info.emptyingFoundation || '-'}</Descriptions.Item>
<Descriptions.Item label="进口底板高程(m)">{data.outletTunnelInletElevation || '-'}</Descriptions.Item> <Descriptions.Item label="进口底板高程(m)">{info.emptyingInletElevation || '-'}</Descriptions.Item>
<Descriptions.Item label="断面尺寸(m)">{data.outletTunnelSectionSize || '-'}</Descriptions.Item> <Descriptions.Item label="断面尺寸(m)">{info.emptyingCrossSection || '-'}</Descriptions.Item>
<Descriptions.Item label="洞长(m)">{data.outletTunnelLength || '-'}</Descriptions.Item> <Descriptions.Item label="洞长(m)">{info.emptyingLength || '-'}</Descriptions.Item>
<Descriptions.Item label="设计流量(m³/s)">{data.outletTunnelDesignFlow || '-'}</Descriptions.Item> <Descriptions.Item label="设计流量(m³/s)">{info.emptyingDesignFlow || '-'}</Descriptions.Item>
<Descriptions.Item label="进口闸门型式">{data.outletTunnelGateType || '-'}</Descriptions.Item> <Descriptions.Item label="进口闸门型式">{info.emptyingGateType || '-'}</Descriptions.Item>
<Descriptions.Item label="进口启闭机型式">{data.outletTunnelHoistType || '-'}</Descriptions.Item> <Descriptions.Item label="进口启闭机型式">{info.emptyingHoistType || '-'}</Descriptions.Item>
</Descriptions> </Descriptions>
</div> </div>
); );

View File

@ -0,0 +1,159 @@
import React, { useMemo,useEffect,useState } from 'react';
import ReactEcharts from 'echarts-for-react';
import { Table,Empty } from 'antd';
import { GetInterval } from '@/utils/tools';
const XlCurve = ({data=[]}) => {
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
render: (text, record, index) => index + 1,
width: 80,
align: 'center',
},
{
title: '水位(m)',
dataIndex: 'z',
key: 'z',
align: 'center',
},
{
title: '流量(m³/s)',
dataIndex: 'q',
key: 'q',
align: 'center',
},
];
const option = useMemo(() => {
if (data.length > 0) {
const maxVal = Math.ceil(Math.max(...data.map(obj => obj.q)))
const minVal = Math.floor(Math.min(...data.map(obj => obj.q)))
const maxValX = Math.max(...data.map(obj => obj.z))
const minValX = Math.min(...data.map(obj => obj.z))
return {
toolbox: {
show: true,
feature: {
saveAsImage: {
show: true,
excludeComponents: ['toolbox'],
pixelRatio: 2,
name:"泄流曲线图"
}
},
right: "14%",
top:"5%"
},
title: {
text: "泄流曲线图",
left: "40%",
textStyle: {
color: '#fff',
}
},
tooltip: {
trigger: 'axis',
},
grid: [
{
top: "10%",
left: "15%",
right: "15%",
bottom: "8%"
},
],
xAxis: [
{
name: "库水位(m)",
nameTextStyle: {
color: '#fff',
},
nameGap: 5,
type: 'value',
min:Math.floor(minValX / 5) *5,
max:Math.ceil(maxValX / 5) *5,
interval: GetInterval(minValX,maxValX),
splitLine: {
show: false
},
axisLabel: {
color: '#fff',
fontSize: 12,
},
}
],
yAxis: [
{
type: 'value',
name: "流量(m³/s)",
nameTextStyle: {
color: '#fff',
},
minInterval:1,
splitLine: {
show: true,
lineStyle: {
color: '#fff',
width: 0.25,
type: 'dotted'
}
},
axisLabel: {
color: '#fff',
fontSize: 12,
},
axisLine: {
show: false
},
axisTick: {
show: false,
},
min: minVal,
max: maxVal
}
],
series: [
{
type: 'line',
color: "#007AFD",
data: data.map(item=>[item.z,item.q]),
smooth: true
},
]
};
}
}, [data]);
return (
<div style={{ display: 'flex', height: '100%', gap: '20px' }}>
<div style={{ flex: '0 0 400px', height: '100%', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<Table
dataSource={data}
columns={columns}
rowKey="id"
pagination={false}
scroll={{ y: 'calc(100vh - 300px)' }} // Estimate height, will adjust if needed
size="small"
bordered={false}
className="custom-table"
/>
</div>
<div style={{ flex: 1, height: '100%', minWidth: 0, paddingRight: 20 }}>
{data.length > 0 ? <ReactEcharts option={option} style={{ height: '100%', width: '100%' }} notMerge={true} /> :
<Empty description={"暂无数据"} />
}
</div>
</div>
);
};
export default XlCurve;

View File

@ -1,16 +1,22 @@
import React, { useState } from 'react'; import React, { useState,useEffect } from 'react';
import { PaperClipOutlined } from '@ant-design/icons'; import { PaperClipOutlined } from '@ant-design/icons';
import PdfView from '@/views/Home/components/UI/PdfView' import PdfView from '@/views/Home/components/UI/PdfView'
import BasicInfo from './components/BasicInfo'; import BasicInfo from './components/BasicInfo';
import MainFeatureParams from './components/MainFeatureParams'; import MainFeatureParams from './components/MainFeatureParams';
import MainBuildingInfo from './components/MainBuildingInfo'; import MainBuildingInfo from './components/MainBuildingInfo';
import CapacityCurve from './components/CapacityCurve';
import XlCurve from './components/XlCurve';
import { httpget,httppost } from '@/utils/request';
import apiurl from '@/service/apiurl';
import './index.less'; import './index.less';
const EngineeringElements = ({ data }) => { const EngineeringElements = ({ data }) => {
const [activeButton, setActiveButton] = useState('basic'); const [activeButton, setActiveButton] = useState('basic');
const [pdfOpen, setPdfOpen] = useState(false) const [pdfOpen, setPdfOpen] = useState(false)
const [filesItem, setFilesItem] = useState({}) const [filesItem, setFilesItem] = useState({})
const [info, setInfo] = useState({}) //主要建筑物信息
const [krLineList, setKrLineList] = useState([]) //库容曲线
const [xrLineList, setXrLineList] = useState([]) //泄流曲线
const buttons = [ const buttons = [
{ label: '工程基础信息', value: 'basic' }, { label: '工程基础信息', value: 'basic' },
{ label: '主要特征参数', value: 'params' }, { label: '主要特征参数', value: 'params' },
@ -18,7 +24,42 @@ const EngineeringElements = ({ data }) => {
{ label: '水库库容曲线', value: 'capacity-curve' }, { label: '水库库容曲线', value: 'capacity-curve' },
{ label: '水库泄流曲线', value: 'discharge-curve' }, { label: '水库泄流曲线', value: 'discharge-curve' },
]; ];
// 建筑物信息
const getBuildInfo = async () => {
try {
const result = await httpget(apiurl.sq.qys.gcys.buildInfo);
if (result.code == 200) {
setInfo(result.data)
}
} catch (error) {
console.log(error);
}
}
// 库容曲线
const getKrList = async () => {
try {
const result = await httppost(apiurl.sq.qys.gcys.krlineList);
if (result.code == 200) {
setKrLineList(result.data)
}
} catch (error) {
console.log(error);
}
}
// 泄流曲线
const getXlList = async () => {
try {
const result = await httppost(apiurl.sq.qys.gcys.xllineList);
if (result.code == 200) {
setXrLineList(result.data)
}
} catch (error) {
console.log(error);
}
}
const handlePreview = (data) => { const handlePreview = (data) => {
setPdfOpen(true) setPdfOpen(true)
setFilesItem(data) setFilesItem(data)
@ -31,7 +72,11 @@ const EngineeringElements = ({ data }) => {
case 'params': case 'params':
return <MainFeatureParams data={data} />; return <MainFeatureParams data={data} />;
case 'buildings': case 'buildings':
return <MainBuildingInfo data={data} />; return <MainBuildingInfo data={info} />;
case 'capacity-curve':
return <CapacityCurve data={krLineList} />;
case 'discharge-curve':
return <XlCurve data={xrLineList} />;
default: default:
return ( return (
<div className="placeholder-content"> <div className="placeholder-content">
@ -41,7 +86,11 @@ const EngineeringElements = ({ data }) => {
); );
} }
}; };
useEffect(() => {
getBuildInfo()
getKrList()
getXlList()
}, [])
return ( return (
<div className="engineering-elements"> <div className="engineering-elements">
<div className="sidebar"> <div className="sidebar">

View File

@ -49,14 +49,21 @@
width: 6px; width: 6px;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2); background: rgba(0, 160, 233, 0.5);
border-radius: 3px; border-radius: 3px;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 160, 233, 0.8);
}
.engineering-descriptions { .engineering-descriptions {
table {
table-layout: fixed !important;
}
.ant-descriptions-view { .ant-descriptions-view {
border-color: rgba(59, 124, 255, 0.3); border-color: rgba(59, 124, 255, 0.3);
} }

View File

@ -0,0 +1,55 @@
import React from 'react';
import { Table } from 'antd';
export default function FlightTasks() {
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
align: 'center',
width: 80,
render: (text, record, index) => index + 1
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: '起飞',
key: 'takeoff',
align: 'center',
width: 120,
render: () => <span className="action-link">起飞</span>
},
{
title: '航线',
key: 'route',
align: 'center',
width: 120,
render: () => <span className="action-link">显示</span>
}
];
const data = [
{ id: 1, name: '视频任务' },
{ id: 2, name: '拍摄' },
{ id: 3, name: '坝体巡检' },
{ id: 4, name: '溢洪道下泄巡查' },
{ id: 5, name: '界桩巡检01' },
];
return (
<div className="tab-content-wrapper">
<Table
columns={columns}
dataSource={data}
pagination={false}
size="middle"
rowKey="id"
/>
</div>
);
}

View File

@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { Table, DatePicker, Button } from 'antd';
import moment from 'moment';
const { RangePicker } = DatePicker;
export default function History() {
const [dates, setDates] = useState([]);
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
align: 'center',
width: 80,
render: (text, record, index) => index + 1
},
{
title: '名称',
dataIndex: 'taskName',
key: 'taskName',
align: 'center',
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
align: 'center',
},
{
title: '结束时间',
dataIndex: 'endTime',
key: 'endTime',
align: 'center',
},
{
title: '操作',
key: 'action',
align: 'center',
width: 100,
render: () => <span className="action-link">回放</span>
}
];
const data = [
{ id: 1, taskName: '视频任务', startTime: '2025-12-15 09:31:23', endTime: '2025-12-15 09:51:23' },
{ id: 2, taskName: '拍摄', startTime: '2025-12-15 09:31:23', endTime: '2025-12-15 09:51:23' },
{ id: 3, taskName: '坝体巡检', startTime: '2025-12-15 09:31:23', endTime: '2025-12-15 09:51:23' },
{ id: 4, taskName: '溢洪道下泄巡查', startTime: '2025-12-15 09:31:23', endTime: '2025-12-15 09:51:23' },
{ id: 5, taskName: '界桩巡检01', startTime: '2025-12-15 09:31:23', endTime: '2025-12-15 09:51:23' },
];
return (
<div className="tab-content-wrapper">
<div className="search-bar" style={{ marginBottom: 16, display: 'flex', alignItems: 'center' }}>
<span style={{ marginRight: 8, color: '#fff' }}>任务时间:</span>
<RangePicker
value={dates}
onChange={setDates}
style={{ width: 300, marginRight: 16 }}
/>
<Button type="primary" className="ant-btn-ghost-blue">查询</Button>
</div>
<Table
columns={columns}
dataSource={data}
pagination={false}
size="middle"
rowKey="id"
className="custom-dark-table"
/>
</div>
);
}

View File

@ -0,0 +1,30 @@
import React from 'react';
import './index.less';
import FlightTasks from './FlightTasks';
import History from './History';
import VideoList from '../VideoList';
export default function UAVModal({ activeKey }) {
const renderContent = () => {
switch (activeKey) {
case '1':
return (
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', color: 'rgba(255,255,255,0.5)' }}>
暂无直播画面
</div>
);
case '2':
return <FlightTasks />;
case '3':
return <History />;
default:
return null;
}
};
return (
<div className="uav-modal-container">
{renderContent()}
</div>
);
}

View File

@ -0,0 +1,25 @@
.uav-modal-container {
height: 100%;
width: 100%;
color: #fff;
// Common Table Styles for Tabs
.tab-content-wrapper {
height: 100%;
padding: 10px;
// background: rgba(255, 255, 255, 0.05);
// border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
.action-link {
color: #1890ff;
cursor: pointer;
margin-right: 10px;
&:hover {
color: #40a9ff;
text-decoration: underline;
}
}
}
}

View File

@ -0,0 +1,27 @@
import { httppost,httpget } from "@/utils/request"
import apiUrl from "@/service/apiurl"
export const treeList = async (params) => {
const res = await httppost(apiUrl.spjk.treeList, params)
return res
}
export const treeListById = async (params) => {
const res = await httpget(`${apiUrl.spjk.treeListById}${params}`, params)
return res
}
export const srcData = async (params) => {
const res = await httpget(`${apiUrl.spjk.srcData}${params}`)
return res
}
export const ysyToken = async (params) => {
const res = await httpget(apiUrl.spjk.ysyToken)
return res
}
export const videoList = async (params) => {
const res = await httppost(apiUrl.spjk.videoList)
return res
}

View File

@ -0,0 +1,80 @@
.videoList {
height: 100%;
}
.videoList .treeRight {
width: 350px;
height: 100%;
padding: 16px 8px;
background-color: #fff;
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
}
.videoList .treeRight .ant-tree-node-selected .iconSelect {
color: blue;
}
.videoList .treeLeft {
flex: 1;
margin-left: 12px;
background-color: #fff;
height: 100%;
padding: 16px 8px;
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
}
.videoList .treeLeft .splitScreen {
height: 100%;
}
.videoList .treeLeft .splitScreen .borderF {
height: calc(100% - 40px);
}
.videoList .treeLeft .splitScreen .borderFa {
width: 100%;
height: 100%;
background: #f6f9fd;
border: 1px solid rgba(42, 117, 214, 0.2);
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
padding-bottom: 10px;
}
.videoList .treeLeft .splitScreen .borderType {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.videoList .treeLeft .splitScreen .borderType .text {
width: 100%;
height: 22px;
font-size: 16px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #79a2d7;
text-align: center;
}
.videoList .treeLeft .splitScreen .videoBorder {
border: 3px solid red;
}
.videoList .treeLeft .splitScreen .videoBorder1 {
flex: 1;
height: 100%;
}
.videoList .treeLeft .splitScreen .videoBorder4 {
width: 50%;
height: 50%;
}
.videoList .treeLeft .splitScreen .videoBorder9 {
width: 33%;
height: 33%;
}

View File

@ -0,0 +1,157 @@
import TreeData from "./treeData"
import './index.less'
import SplitScreen from "./splitScreen"
import { useEffect, useState } from "react"
import { treeList, srcData, videoList, ysyToken } from './http'
import VideoControler from "@/components/VideoCom/VideoControler"
import { httppost } from "@/utils/request"
import apiurl from "@/service/apiurl"
const VideoList = () => {
const [videoArr, setVideo] = useState([])
const [count, setCount] = useState(999)
const [indexData, setIndex] = useState(999)
const [size, setSize] = useState(1)
const [treeListData, setTreeData] = useState([])
const [selectList, setSelectList] = useState()
const selectedKeys = async (list, node) => {
console.log(node, 'node');
let src = null
let obj = {}
setSelectList(list[list.length - 1])
if (indexData === 999) {
if (node.selected) {
console.log(node.node.relType, 'asdkjashkd');
src = node.node.relType == "ysy" ? (await ysyToken(node.node.indexCode)).data : (await srcData(node.node.indexCode)).data
obj = { ...node.node, src: src }
if (videoArr.length < size) {
setVideo([...videoArr, obj])
} else {
videoArr[size - 1] = obj
console.log(videoArr, '45678');
setVideo((videoArr) => {
const newA = [...videoArr]
return newA
})
}
} else {
let count = videoArr.findIndex(item => item.id === node.node.id)
setVideo(() => {
const newA = [...videoArr]
newA.splice(count, 1)
return newA
})
}
} else {
if (node.selected) {
src = node.node.relType == 'ysy' ? (await ysyToken(node.node.indexCode)).data : (await srcData(node.node.indexCode)).data
obj = { ...node.node, src: src }
videoArr[indexData] = obj
setVideo((videoArr) => {
const newA = [...videoArr]
return newA
})
}
}
}
const clickIndex = (index, item) => {
setIndex(index)
setSelectList(videoArr[index])
}
const getType = (size) => {
console.log(size, videoArr.slice(0, size), 'szie');
setSize(size)
setIndex(999)
setVideo(() => {
const newA = videoArr.slice(0, size)
console.log(newA);
return newA
})
}
const getTreeData = async () => {
const res = await treeList()
const res1 = await videoList()
const arr = res1.data.filter(item => item.menuId).map((item, index) => {
item.parentId = item.menuId
item.id = 999 + index
item.isLeaf = true
// item.icon=<VideoCameraFilled />
return item
})
const arr1 = [...arr, ...res.data]
console.log("before", arr1);
setTreeData(buildTree(arr1))
}
function buildTree(data, parentId = "0") {
const tree = [];
for (const item of data) {
if (item.parentId == parentId) {
const children = buildTree(data, item.id);
if (children.length > 0) item.children = children;
tree.push(item)
}
}
return tree;
}
let timer = null;
// 云台控制
const onOperation = async (params) => {
let data = {
...params,
indexCode: selectList?.indexCode,
action: 0
}
try {
const res = await httppost(apiurl.spjk.controler, data)
if (res.code == 200) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
onOperation1(params)
}, 500)
}
} catch (error) {
console.log(error);
}
}
const onOperation1 = async (params) => {
let data = {
...params,
indexCode: selectList?.indexCode,
action: 1
}
try {
const res = await httppost(apiurl.spjk.controler, data)
} catch (error) {
console.log(error);
}
}
useEffect(() => {
getTreeData()
}, [])
return (
<div className='flex videoList'>
<div className={['treeRight', (selectList && selectList.type == 1) ? 'ptz-visible' : ''].join(' ')}>
<TreeData size={size} selectedKeys={selectedKeys} treeListData={treeListData} videoArr={videoArr} />
{
selectList && selectList.type == 1 ?
<div style={{ position: "absolute", bottom: 0, left: 0 }}>
<VideoControler
selectItem={selectList}
onOperation={onOperation}
/>
</div>
: null
}
</div>
<div className='treeLeft'><SplitScreen count={count} videoArr={videoArr} clickIndex={clickIndex} getType={getType} /></div>
</div>
)
}
export default VideoList

View File

@ -0,0 +1,114 @@
.videoList{
height: 100%;
.treeRight{
position: relative;
width: 350px;
height: 100%;
padding: 16px 8px;
color: #fff;
border-radius: 4px;
&.ptz-visible{
padding-bottom: 260px;
}
.ant-tree-iconEle{
.iconSelect{
// color: blue;
width: 25px;
height: 25px;
margin-bottom: 5px;
}
}
.ant-tree {
background: transparent;
color: #fff;
.ant-tree-node-content-wrapper {
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
&.ant-tree-node-selected {
background-color: rgba(24, 144, 255, 0.3);
}
}
.ant-tree-title {
color: #fff;
}
.ant-tree-switcher {
color: #fff;
}
}
}
.treeLeft{
flex: 1;
margin-left: 12px;
// background-color: rgba(255, 255, 255, 0.05);
color: #fff;
height: 100%;
padding: 16px 8px;
border-radius: 4px;
// border: 1px solid rgba(255, 255, 255, 0.1);
.splitScreen{
height: 100%;
.borderF{
height: 100%;
// padding: 20px;
}
.borderFa{
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
padding-bottom: 10px;
}
.borderType{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.text{
width: 100%;
height: 22px;
font-size: 16px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: rgba(255, 255, 255, 0.6);
text-align: center;
}
}
.videoBorder{
border: 2px solid #1890ff;
}
.videoBorder1{
flex: 1;
height: 100%;
}
.videoBorder4{
width: 50%;
height: 50%;
}
.videoBorder9{
width: 33%;
height: 33%;
}
}
}
}

View File

@ -0,0 +1,100 @@
import TreeData from "./treeData"
import './index.less'
import { Radio, Tabs, Empty } from "antd"
import { useEffect, useRef, useState } from "react";
import HFivePlayer from "@/components/VideoCom/videoPlary";
// import HFivePlayer from "./video1";
const SplitScreen: React.FC = (props: any) => {
const [size, setSize] = useState(1);
const [list, setList] = useState<any>([]);
const [videoList, setVideoList] = useState<any>([]);
const [num, setNum] = useState<any>(999);
const [styleType, setStyle] = useState(false);
const onChange = (e: any) => {
setSize(Number(e));
handDate(Number(e))
setNum(999)
};
const handDate = (size: any) => {
props.getType(size)
console.log(new Array(size).fill(1),'4678999');
setList(new Array(size).fill(1))
setVideoList([])
}
const clickVideo = (index: any,item:any) => {
if(num == index){
props.clickIndex(999,item)
setStyle(true)
setNum(999)
}else{
props.clickIndex(index,item)
setStyle(false)
setNum(index)
}
}
const getNum = () => {
if (num === 999) {
// return props.videoArr.length - 1
} else {
return num
}
}
useEffect(() => {
if (props.count !== 999) {
setNum(props.count)
}
}, [props.count])
useEffect(() => {
setVideoList(props.videoArr)
console.log(props.videoArr,'props.videoArr');
}, [props.videoArr])
useEffect(() => {
onChange('4')
}, [])
const items = [
{ key: '1', label: '单屏' },
{ key: '4', label: '四分屏' },
{ key: '9', label: '九分屏' },
]
return (
<div className='splitScreen'>
{/* <Tabs
onChange={onChange}
defaultActiveKey='4'
type="card"
items={items}
/> */}
{/* {props.videoArr} */}
<div className={['flex', 'flexwarp', 'borderF'].join(' ')} style={{ position: 'relative', height: '100%' }}>
{list.map((item: any, index: any) => {
return <div onClick={() => clickVideo(index,item)}
// onClick={()=>clickVideo(index)}
className={[getNum() == index ? 'videoBorder' : null,
size == 1 ? 'videoBorder1' : null, size == 4 ? 'videoBorder4' : null,
size == 9 ? 'videoBorder9' : null,'borderFa'].join(' ')}>
{props.videoArr[index]&&<div style={{height:'100%',width:'100%'}}>
<div style={{ height: 'calc(100% - 20px)',width:'100%' }}>
{videoList.length&&<HFivePlayer size={size} wsUrl={videoList[index]} playerID={index} />}
</div>
<div style={{ textAlign: 'center' }}>{videoList[index]?.name}</div>
</div>}
{!props.videoArr[index]&&<div className='borderType'>
<img src={`${process.env.PUBLIC_URL}/assets/images/no-video.png`} alt=""/>
<div className='text'></div>
</div>}
</div>
})}
</div>
</div>
)
}
export default SplitScreen

View File

@ -0,0 +1,104 @@
import { DownOutlined, VideoCameraFilled } from '@ant-design/icons';
import React, { useEffect, useState } from 'react';
import { Tree } from 'antd';
interface DataNode {
title: string;
key: string;
isLeaf?: boolean;
children?: DataNode[];
}
let arr:any=[]
// It's just a simple demo. You can use tree map to optimize update perf.
const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] =>
list.map((node: any) => {
if (node.id === key) {
return {
...node,
children,
};
}
if (node.children) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
return node;
});
const TreeData = (props: any) => {
console.log(props.treeListData);
const [checkNode, setCheckNode] = useState([]);
const onSelect = async (selectedKeys: (any | never[]), info: any) => {
if(!info.node.isLeaf && !info.node.selected)return
if(selectedKeys.length < props.size){
setCheckNode(selectedKeys)
props.selectedKeys(info.selectedNodes,info)
}else{
setCheckNode((selectedKeys) => {
const newA:any = [...selectedKeys]
newA[Number(props.size)-1]=info.node.id
const arr =newA.map((item:any)=>{
let count = info.selectedNodes.findIndex((itemq:any) => itemq.id === item)
if(count !== -1){
return info.selectedNodes[count]
}
})
props.selectedKeys(arr,info)
return newA
})
}
};
const iconSelect = (a: any) => {
const type = a.data.type;
// const online = a.data.online
if (type == 1) {
return <img src={`${process.env.PUBLIC_URL}/assets/images/qiujiG.png`} alt='' className='iconSelect'/>
} else if(type ==2) {
return <img
src={`${process.env.PUBLIC_URL}/assets/images/jk.png`}
alt=''
className='iconSelect'
style={{width:17,height:18}}
/>
}
// if (a.data.isLeaf) {
// if(a.data.type ==1)
// return <VideoCameraFilled className='iconSelect'/>
// }
}
useEffect(()=>{
setCheckNode(() => {
const newA = props.videoArr.map((item:any)=>{
return item?.id
})
return newA
})
// setCheckNode(selectedKeys)
},[props.videoArr])
// useEffect(()=>{})
return <div style={{height:'100%',overflowY:'scroll'}}>
{props.treeListData.length !== 0 &&
<Tree
treeData={props.treeListData}
fieldNames={{ title: 'name', key: 'id' }}
defaultExpandAll={true}
multiple
showIcon
key='id'
// height={600}
onSelect={onSelect}
icon={iconSelect}
selectedKeys={checkNode}
/> }
</div>;
};
export default TreeData;

View File

@ -69,7 +69,7 @@
align-items: center; align-items: center;
flex: 1; flex: 1;
font-size: 14px; font-size: 14px;
justify-content: space-between;
.label { .label {
color: #CCF3FF; color: #CCF3FF;
margin-right: 10px; margin-right: 10px;

View File

@ -6,8 +6,10 @@ import MonitoringElements from './components/MonitoringElements';
import ReservoirAreaElements from './components/ModalComponents/ReservoirAreaElements'; import ReservoirAreaElements from './components/ModalComponents/ReservoirAreaElements';
import EngineeringElements from './components/ModalComponents/EngineeringElements'; import EngineeringElements from './components/ModalComponents/EngineeringElements';
import DownstreamElements from './components/ModalComponents/DownstreamElements'; import DownstreamElements from './components/ModalComponents/DownstreamElements';
// import ManagementElements from './components/ModalComponents/ManagementElements'; import AllWeatherControl from './components/AllWeatherControl';
// import AllWeatherMastery from './components/AllWeatherMastery'; import ManagementCycle from './components/ManagementCycle';
import CycleArchive from './components/ModalComponents/CycleArchive';
import AllWeatherModal from './components/ModalComponents/AllWeatherModal';
import CommonModal from '../../UI/CommonModal'; import CommonModal from '../../UI/CommonModal';
import { httppost } from '@/utils/request'; import { httppost } from '@/utils/request';
import apiurl from '@/service/apiurl'; import apiurl from '@/service/apiurl';
@ -16,6 +18,7 @@ import './index.less';
const SiQuan = () => { const SiQuan = () => {
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [modalType, setModalType] = useState('monitor'); // 'monitor' | 'cycle' | 'allweather'
const [infos, setInfos] = useState({}); const [infos, setInfos] = useState({});
const [activeTab, setActiveTab] = useState('kqys'); // Default active tab const [activeTab, setActiveTab] = useState('kqys'); // Default active tab
@ -23,7 +26,12 @@ const SiQuan = () => {
{ label: '库区要素', value: 'kqys' }, { label: '库区要素', value: 'kqys' },
{ label: '工程要素', value: 'gcys' }, { label: '工程要素', value: 'gcys' },
{ label: '下游要素', value: 'xyys' }, { label: '下游要素', value: 'xyys' },
// { label: '管理要素', value: 'glys' }, ];
const tabsAllWeather = [
{ label: '雨情监测', value: 'rain' },
{ label: '水库水情', value: 'reservoir' },
{ label: '出入库流量', value: 'flow' },
{ label: '安全监测', value: 'safety' },
]; ];
const getInfo = async () => { const getInfo = async () => {
@ -43,6 +51,17 @@ const SiQuan = () => {
}, []); }, []);
const handleOpenModal = () => { const handleOpenModal = () => {
setModalType('monitor');
setModalVisible(true);
};
const handleOpenCycleModal = () => {
setModalType('cycle');
setModalVisible(true);
};
const handleOpenAllWeatherModal = () => {
setModalType('allweather');
setActiveTab('rain');
setModalVisible(true); setModalVisible(true);
}; };
@ -64,7 +83,7 @@ const SiQuan = () => {
<SupervisionCoverage data={infos} /> <SupervisionCoverage data={infos} />
</CommonCard> </CommonCard>
<CommonCard <CommonCard
title="监测全要素" title="掌握全要素"
className="panel-card card-2" className="panel-card card-2"
headerExtra={<ThreeDots onClick={handleOpenModal} />} headerExtra={<ThreeDots onClick={handleOpenModal} />}
> >
@ -77,12 +96,16 @@ const SiQuan = () => {
<CommonCard <CommonCard
title="管控全天候" title="管控全天候"
className="panel-card card-1" className="panel-card card-1"
headerExtra={<ThreeDots onClick={() => console.log('管控全天候 clicked')} />} headerExtra={<ThreeDots onClick={handleOpenAllWeatherModal} />}
> >
<div className="placeholder-content">内容填充区域</div> <AllWeatherControl />
</CommonCard> </CommonCard>
<CommonCard title="管理全周期" className="panel-card card-3"> <CommonCard
<div className="placeholder-content">内容填充区域</div> title="管理全周期"
className="panel-card card-3"
headerExtra={<ThreeDots onClick={handleOpenCycleModal} />}
>
<ManagementCycle />
</CommonCard> </CommonCard>
</div> </div>
</div> </div>
@ -90,17 +113,23 @@ const SiQuan = () => {
<CommonModal <CommonModal
visible={modalVisible} visible={modalVisible}
onClose={handleCloseModal} onClose={handleCloseModal}
title="掌握全要素" title={modalType === 'monitor' ? "掌握全要素" : (modalType === 'cycle' ? "全周期档案" : "管控全天候")}
tabs={tabs} tabs={modalType === 'monitor' ? tabs : (modalType === 'allweather' ? tabsAllWeather : [])}
activeTab={activeTab} activeTab={activeTab}
onTabChange={handleTabChange} onTabChange={handleTabChange}
width={modalType === 'cycle' ? '70%':modalType === 'allweather' ? '90%': undefined}
> >
{/* Content changes based on activeTab */} {/* Content changes based on activeTab */}
<div style={{color: '#fff', height: '100%' }}> <div style={{color: '#fff', height: '100%' }}>
{activeTab === 'kqys' && <ReservoirAreaElements />} {modalType === 'monitor' && (
{activeTab === 'gcys' && <EngineeringElements data={infos} />} <>
{activeTab === 'xyys' && <DownstreamElements />} {activeTab === 'kqys' && <ReservoirAreaElements />}
{/* {activeTab === 'glys' && <ManagementElements />} */} {activeTab === 'gcys' && <EngineeringElements data={infos} />}
{activeTab === 'xyys' && <DownstreamElements />}
</>
)}
{modalType === 'cycle' && <CycleArchive />}
{modalType === 'allweather' && <AllWeatherModal active={activeTab} />}
</div> </div>
</CommonModal> </CommonModal>
</div> </div>

View File

@ -31,8 +31,8 @@
} }
.right-part { .right-part {
.card-1 { flex: 4; } .card-1 { flex: 5; }
.card-2 { flex: 3; } .card-2 { flex: 3; }
.card-3 { flex: 1; } .card-3 { flex: 2; }
} }
} }

View File

@ -0,0 +1,46 @@
import React, { useState, useEffect } from 'react';
import { Table, Input, Button } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import CommonModal from '@/views/Home/components/UI/CommonModal';
import usePageTable from '@/components/crud/usePageTable';
import { createCrudService } from '@/components/crud/_';
import apiurl from '@/service/apiurl';
const OrgnizeModal = ({ visible, onClose,title }) => {
const columns = [
{ title: '序号', dataIndex: 'inx', key: 'inx', width: 60, align: 'center' },
{ title: '用户姓名', dataIndex: 'spec', key: 'spec', align: 'center' },
{ title: '手机号码', dataIndex: 'phone', key: 'phone', align: 'center' },
{ title: '部门', dataIndex: 'contactPerson', key: 'contactPerson', align: 'center' },
];
const { tableProps, search} = usePageTable(createCrudService(apiurl.sz.jqjz.wzPage).find);
useEffect(() => {
if (visible) {
search()
}
}, [visible]);
return (
<CommonModal
visible={visible}
onClose={onClose}
title={title}
width="70%"
>
<div className="material-modal-content">
<div className="table-wrapper">
<Table
columns={columns}
{...tableProps}
className="custom-table"
size="middle"
/>
</div>
</div>
</CommonModal>
);
};
export default OrgnizeModal;

View File

@ -0,0 +1,115 @@
import React, { useState, useEffect } from 'react';
import { UserOutlined, BankOutlined, ApartmentOutlined } from '@ant-design/icons';
import textBg from '@/assets/images/card/textbg.png';
import arrowIcon from '@/assets/images/card/arrow.png';
import OrgnizeModal from './OrgnizeModal'
import './index.less';
const PerfectSystem = ({ data }) => {
const [modalVisible, setModalVisible] = useState(false);
const [modalTitle, setModalTitle] = useState('');
const managementInfo = [
{ label: '管理单位', value: data?.managName ?? '-', icon: <BankOutlined />, type: 'unit' },
{ label: '负责人', value: data?.chargePerson ?? '-', icon: <UserOutlined />, type: 'person' },
{ label: '归口管理部门', value: data?.admDep ?? '-', icon: <ApartmentOutlined />, type: 'dept' },
];
const leftOrg = [
{ name: '工程科', count: 3 },
{ name: '办公室', count: 2 },
{ name: '财务科', count: 1 },
{ name: '...', count: null },
];
const rightOrg = [
{ name: '后勤保障', subName: '中心', count: 5 },
{ name: '...', count: null },
];
const handleCardClick = (item) => {
setModalTitle(item.name);
setModalVisible(true);
};
return (
<div className="perfect-system">
<div className="section">
<div className="section-title">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span>管理单位</span>
</div>
<div className="info-list">
{managementInfo.map((item, index) => (
<div key={index} className="info-item" style={{ backgroundImage: `url(${textBg})` }}>
<div className={`icon-box ${item.type}`}>
{item.icon}
</div>
<div className="info-content">
<span className="label">{item.label}</span>
<span className="value">{item.value}</span>
</div>
</div>
))}
</div>
</div>
<div className="section mt-15">
<div className="section-title">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span>水库组织机构</span>
</div>
<div className="org-chart">
<div className="side-column left">
{leftOrg.map((item, idx) => (
<div
key={idx}
className={`org-node ${item.count === null ? 'placeholder' : ''}`}
style={{cursor:'pointer'}}
onClick={() => handleCardClick(item)}
>
<span className="node-text" style={{borderBottom:"1px solid #00a0e9"}}>{item.name}{item.count !== null ? `(${item.count})` : ''}</span>
<div className="connector-line"></div>
</div>
))}
</div>
<div className="center-column">
<div className="main-node">
<span className="vertical-text">双石水库管理处</span>
</div>
</div>
<div className="side-column right">
{rightOrg.map((item, idx) => (
<div
key={idx}
className={`org-node ${item.count === null ? 'placeholder' : ''}`}
style={{cursor:'pointer'}}
onClick={() => handleCardClick(item)}
>
<div className="connector-line"></div>
<span className="node-text" style={{borderBottom:"1px solid #00a0e9"}}>
{item.name}
{item.subName && <><br />{item.subName}</>}
{item.count !== null ? `(${item.count})` : ''}
</span>
</div>
))}
{/* Fill empty space to match left side height roughly */}
<div className="org-node placeholder" style={{ opacity: 0 }}>
<div className="connector-line"></div>
</div>
<div className="org-node placeholder" style={{ opacity: 0 }}>
<div className="connector-line"></div>
</div>
</div>
</div>
<OrgnizeModal
visible={modalVisible}
title={modalTitle}
onClose={() => setModalVisible(false)}
/>
</div>
</div>
);
};
export default PerfectSystem;

View File

@ -0,0 +1,206 @@
.perfect-system {
width: 100%;
height: 100%;
color: #fff;
padding: 5px;
overflow-y: auto;
// Scrollbar hidden
&::-webkit-scrollbar {
display: none;
}
.section {
margin-bottom: 10px;
&.mt-15 {
margin-top: 15px;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 10px;
.arrow-icon {
width: 20px;
height: 18px;
margin-right: 8px;
object-fit: contain;
}
span {
font-size: 14px;
color: #fff;
text-shadow: 0 0 5px rgba(0, 160, 233, 0.5);
}
}
.info-list {
display: flex;
flex-direction: column;
gap: 5px;
.info-item {
display: flex;
align-items: center;
padding: 5px 10px;
background-size: 100% 100%;
background-repeat: no-repeat;
.icon-box {
width: 18px;
height: 18px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
margin-right: 15px;
font-size: 18px;
&.unit {
background: linear-gradient(135deg, #1890ff 0%, #0050b3 100%);
}
&.person {
background: linear-gradient(135deg, #00eaff 0%, #006d75 100%);
}
&.dept {
background: linear-gradient(135deg, #722ed1 0%, #391085 100%);
}
}
.info-content {
display: flex;
align-items: center;
flex: 1;
font-size: 14px;
justify-content: space-between;
.label {
color: #CCF3FF;
margin-right: 10px;
min-width: 120px;
}
.value {
color: #CCF3FF;
font-weight: 500;
}
}
}
}
.org-chart {
display: flex;
justify-content: space-between;
align-items: stretch;
padding: 0 5px;
height: 200px;
position: relative;
.center-column {
flex: 0 0 60px;
display: flex;
justify-content: center;
align-items: center;
z-index: 2;
.main-node {
background: url('../../../../../../../assets/images/card/ognize.png');
background-size: 100% 100%;
background-repeat: no-repeat;
padding: 10px 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.vertical-text {
writing-mode: vertical-rl;
font-size: 16px;
letter-spacing: 4px;
color: #fff;
text-orientation: upright;
}
}
}
.side-column {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 5px 0;
position: relative;
.org-node {
position: relative;
background: url('../../../../../../../assets/images/card/smallCard.png');
background-size: 100% 100%;
background-repeat: no-repeat;
border-radius: 2px;
padding: 5px;
text-align: center;
color: #fff;
font-size: 13px;
min-height: 32px;
display: flex;
align-items: center;
justify-content: center;
width: 90px;
box-shadow: inset 0 0 10px rgba(0, 160, 233, 0.2);
&.placeholder {
background: transparent;
border: none;
box-shadow: none;
.node-text {
font-size: 20px;
color: rgba(255, 255, 255, 0.5);
// border-bottom: 1px solid #00a0e9;
}
.connector-line {
border-top-color: rgba(0, 160, 233, 0.3);
}
}
.connector-line {
position: absolute;
top: 50%;
height: 1px;
border-top: 1px dashed #00a0e9;
z-index: 1;
}
}
&.left {
align-items: flex-start;
.org-node {
.connector-line {
left: 100%;
width: 500px; // Large width to ensure it reaches center
z-index: -1;
}
}
}
&.right {
align-items: flex-end;
.org-node {
.connector-line {
right: 100%;
width: 500px; // Large width to ensure it reaches center
z-index: -1;
}
}
}
}
}
}
}

View File

@ -0,0 +1,62 @@
import React, { useState, useEffect } from 'react';
import CommonModal from '@/views/Home/components/UI/CommonModal';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import './index.less';
const GalleryModal = ({ visible, onClose, title, data = [] }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
if (visible) {
setSelectedIndex(0);
}
}, [visible]);
if (!visible) return null;
const currentItem = data[selectedIndex] || {};
const handlePrev = () => {
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : data.length - 1));
};
const handleNext = () => {
setSelectedIndex((prev) => (prev < data.length - 1 ? prev + 1 : 0));
};
return (
<CommonModal
visible={visible}
onClose={onClose}
title={title}
width={'60%'}
>
<div className="gallery-modal-content">
<div className="main-image-container">
<img src={currentItem.url} alt={currentItem.name} className="main-image" />
</div>
<div className="image-info">
图片名称{currentItem.name}
</div>
<div className="thumbnail-strip">
<div className="scroll-btn left" onClick={handlePrev}><LeftOutlined /></div>
<div className="thumbnails-wrapper">
{data.map((item, index) => (
<div
key={index}
className={`thumbnail-item ${index === selectedIndex ? 'thumb-active' : ''}`}
onClick={() => setSelectedIndex(index)}
>
<img src={item.url} alt={item.name} />
</div>
))}
</div>
<div className="scroll-btn right" onClick={handleNext}><RightOutlined /></div>
</div>
</div>
</CommonModal>
);
};
export default GalleryModal;

View File

@ -0,0 +1,116 @@
.gallery-modal-content {
display: flex;
flex-direction: column;
height: 700px;
color: #fff;
.main-image-container {
flex: 1;
position: relative;
display: flex;
justify-content: center;
align-items: center;
background: #000;
overflow: hidden;
.main-image {
width: 100%;
height: 100%;
// object-fit: contain;
}
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 40px;
// background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 20px;
color: #fff;
transition: all 0.3s;
&:hover {
background: rgba(0, 160, 233, 0.8);
}
&.prev { left: 20px; }
&.next { right: 20px; }
}
}
.image-info {
height: 40px;
line-height: 40px;
text-align: center;
font-size: 14px;
}
.thumbnail-strip {
height: 100px;
// background: #111;
display: flex;
align-items: center;
padding: 0 10px;
border-top: 1px solid #0181e6;
.scroll-btn {
width: 30px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 18px;
&:hover { color: #00a0e9; }
}
.thumbnails-wrapper {
flex: 1;
display: flex;
gap: 10px;
overflow-x: auto;
padding: 10px;
&::-webkit-scrollbar {
height: 4px;
background: #333;
}
&::-webkit-scrollbar-thumb {
background: #666;
border-radius: 2px;
}
.thumbnail-item {
width: 120px;
height: 70px;
flex-shrink: 0;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
user-select: none;
outline: none;
background: transparent;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
&.thumb-active {
border-color: #00a0e9;
}
&:hover {
border-color: rgba(0, 160, 233, 0.6);
}
}
}
}
}

View File

@ -0,0 +1,109 @@
import React, { useState, useEffect } from 'react';
import { Table, Input, Button } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import CommonModal from '@/views/Home/components/UI/CommonModal';
import usePageTable from '@/components/crud/usePageTable';
import { createCrudService } from '@/components/crud/_';
import apiurl from '@/service/apiurl';
import './index.less';
const MaterialModal = ({ visible, onClose }) => {
const unitType = {
1:'个',
2:'件',
3:'米',
4:'把',
5:'台',
6:'套',
7:'副',
8:'箱',
9:'卷',
10:'立方米',
11:'平方米',
}
const [searchText, setSearchText] = useState('');
const columns = [
{ title: '序号', dataIndex: 'inx', key: 'inx', width: 60, align: 'center' },
{
title: '物资名称',
dataIndex: 'goodsName',
key: 'goodsName',
align: 'center',
sorter: (a, b) => (a.goodsName || '').localeCompare(b.goodsName || ''),
showSorterTooltip: { title: '整理' }
},
{
title: '物资类型', dataIndex: 'goodsType', key: 'goodsType', align: 'center',
render: (_, record) => <span>{ record.goodsType === 1 ? "抢险物资" : "救生器材"}</span>
},
{ title: '规格', dataIndex: 'spec', key: 'spec', align: 'center' },
{
title: '单位', dataIndex: 'unit', key: 'unit', align: 'center',
render: (v) => <span>{unitType[v]}</span>
},
{ title: '库存数量', dataIndex: 'storeQuantity', key: 'storeQuantity', align: 'center' },
{
title: '仓库地点',
dataIndex: 'storeLocation',
key: 'storeLocation',
align: 'center',
sorter: (a, b) => (a.storeLocation || '').localeCompare(b.storeLocation || ''),
showSorterTooltip: { title: '整理' }
},
{ title: '联系人', dataIndex: 'contactPerson', key: 'contactPerson', align: 'center' },
{ title: '联系电话', dataIndex: 'phone', key: 'phone', align: 'center' },
];
const { tableProps, search} = usePageTable(createCrudService(apiurl.sz.jqjz.wzPage).find);
const handleSearch = () => {
let params = {
search: {
goodsName:searchText
}
}
search(params);
};
useEffect(() => {
if (visible) {
handleSearch()
}
setSearchText('')
}, [visible]);
return (
<CommonModal
visible={visible}
onClose={onClose}
title="防汛物资"
width="70%"
>
<div className="material-modal-content">
<div className="search-bar">
<span className="label">物资名称</span>
<Input
value={searchText}
onChange={e => setSearchText(e.target.value)}
placeholder="请输入物资名称"
style={{ width: 240 }}
allowClear
/>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch} className="search-btn ant-btn-ghost-blue">查询</Button>
</div>
<div className="table-wrapper">
<Table
columns={columns}
{...tableProps}
className="custom-table"
size="middle"
/>
</div>
</div>
</CommonModal>
);
};
export default MaterialModal;

View File

@ -0,0 +1,24 @@
.material-modal-content {
padding: 10px;
color: #fff;
.search-bar {
display: flex;
align-items: center;
margin-bottom: 20px;
.label {
font-size: 14px;
margin-right: 10px;
color: #fff;
}
.search-btn {
margin-left: 10px;
}
}
}

View File

@ -0,0 +1,187 @@
import React, { useState,useEffect } from 'react';
import moment from 'moment';
import arrowIcon from '@/assets/images/card/arrow.png';
import smallCard from '@/assets/images/card/smallCard.png';
import homeImg from '@/assets/images/home.png'; // Placeholder for gallery
import jingfeiIcon from '@/assets/images/card/jingfei.png';
import lightBg from '@/assets/images/card/light.png';
import GalleryModal from './GalleryModal';
import MaterialModal from './MaterialModal';
import YearSelect from '@/views/Home/components/UI/YearSelect';
import apiurl from '@/service/apiurl';
import { httpget } from '@/utils/request';
import { config } from '@/config';
import './index.less';
const SoundMechanism = () => {
const [year, setYear] = useState(moment().format('YYYY'));
const [modalVisible, setModalVisible] = useState(false);
const [materialVisible, setMaterialVisible] = useState(false);
const [modalTitle, setModalTitle] = useState('');
const [galleryData, setGalleryData] = useState([]);
const [manageInfo, setManageInfo] = useState({}) //管理设施
const [budgetInfo, setBudgetInfo] = useState({}) //经费
// Mock data for gallery
const houseImages = [
{ name: '管理用房.jpg', url: homeImg },
{ name: '监控室.jpg', url: homeImg },
{ name: '水库全景.jpg', url: homeImg },
{ name: '会议室.jpg', url: homeImg },
{ name: '值班室.jpg', url: homeImg },
{ name: '物资仓库.jpg', url: homeImg },
];
// Facility Data (Mocked as per UI)
const facilities = [
{ value: manageInfo?.managementHousing??'-', unit: 'm²', label: '管理用房', underline: true, clickable: true},
{ value: manageInfo?.rainWaterCount??'-', unit: '个', label: '雨水情测报' },
{ value: manageInfo?.safeCheckCount??'-', unit: '个', label: '安全监测设施' },
{ value: manageInfo?.cctvCount??'-', unit: '个', label: '视频监控设施' },
{ value: manageInfo?.goodsTypeCount??'-', unit: '项', label: '防汛物资种类', underline: true, clickable: true, type: 'material' },
{ value: manageInfo?.roadLength??'-', unit: '米', label: '防汛道路' },
];
const handleCardClick = (item) => {
if (!item.clickable) return;
if (item.label == '管理用房') {
getManagePic()
} else if (item.type === 'material') {
setMaterialVisible(true);
} else {
setModalTitle(item.label);
setModalVisible(true);
}
};
// Funding Data (Mocked as per UI)
const fundingData = [
{ label: '年度收入预算', value: budgetInfo?.annualExpenditureBudget ?? '-', unit: '万元' },
{ label: '年度支出预算', value: budgetInfo?.annualIncomeBudget ?? '-', unit: '万元' },
];
// 获取管理设施
const getManage= async () => {
try {
const result = await httpget(apiurl.sz.jqjz.manageInfo)
if (result.code == 200) {
setManageInfo(result?.data)
}
} catch (error) {
console.log(error);
}
}
// 获取管理用房图片
const getManagePic= async () => {
try {
const result = await httpget(apiurl.sz.jqjz.managePic)
if (result.code == 200) {
const files = result.data?.files || [];
if (files.length > 0) {
setGalleryData(files.map(item=>({name:item.fileName,url:config.minioIp +item.filePath})))
setModalTitle('管理用房');
setModalVisible(true);
}
}
} catch (error) {
console.log(error);
}
}
// 获取年度预算
const getBudget = async (params) => {
try {
const result = await httpget(apiurl.sz.jqjz.budgetInfo + params)
if (result.code == 200) {
setBudgetInfo(result.data)
}
} catch (error) {
console.log(error);
}
}
useEffect(() => {
getBudget(year)
}, [year])
useEffect(() => {
getManage()
}, [])
return (
<div className="sound-mechanism">
{/* Section 1: Management Facilities */}
<div className="section">
<div className="section-title">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span>管理设施</span>
</div>
<div className="facility-grid">
{facilities.map((item, index) => (
<div
key={index}
className={`facility-card ${item.clickable ? 'clickable' : ''}`}
style={{ backgroundImage: `url(${smallCard})` }}
onClick={() => handleCardClick(item)}
>
<div className={`value-wrapper ${item.underline ? 'underlined' : ''}`}>
<span className="value">{item.value}</span>
<span className="unit">{item.unit}</span>
</div>
<div className="label">{item.label}</div>
</div>
))}
</div>
</div>
<GalleryModal
visible={modalVisible}
title={modalTitle}
onClose={() => setModalVisible(false)}
data={galleryData}
/>
<MaterialModal
visible={materialVisible}
onClose={() => setMaterialVisible(false)}
/>
{/* Section 2: Funding Guarantee */}
<div className="section mt-15">
<div className="section-header">
<div className="title-wrapper">
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span>经费保障</span>
</div>
<YearSelect
value={year}
onChange={setYear}
/>
</div>
<div className="funding-container">
{fundingData.map((item, index) => (
<div key={index} className="funding-item">
<div className="icon-wrapper" style={{width:40}}>
<img src={jingfeiIcon} alt="icon" className="jingfei-icon" />
</div>
<div
className="content"
style={{ backgroundImage: `url(${lightBg})` }}
>
<div className="value-row">
<span className="value">{item.value}</span>
<span className="unit">{item.unit}</span>
</div>
<div className="label">{item.label}</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default SoundMechanism;

View File

@ -0,0 +1,160 @@
.sound-mechanism {
width: 100%;
height: 100%;
color: #fff;
padding: 5px;
overflow-y: hidden;
// Scrollbar hidden
&::-webkit-scrollbar {
display: none;
}
.section {
margin-bottom: 0px;
&.mt-15 { margin-top: 5px; }
.section-title, .section-header .title-wrapper {
display: flex;
align-items: center;
margin-bottom: 10px;
.arrow-icon {
width: 20px;
height: 18px;
margin-right: 8px;
object-fit: contain;
}
span {
font-size: 14px;
color: #fff;
text-shadow: 0 0 5px rgba(0, 160, 233, 0.5);
}
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.facility-grid {
display: flex;
flex-wrap: wrap;
.facility-card {
width: calc((100% - 20px) / 3);
height: 70px;
margin-bottom: 15px;
margin-right: 10px;
&:nth-child(3n) { margin-right: 0; }
background-size: 100% 100%;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 5px;
&.clickable {
cursor: pointer;
}
.value-wrapper {
margin-bottom: 4px;
&.underlined {
border-bottom: 1px solid #00a0e9;
padding-bottom: 2px;
}
.value {
font-size: 20px;
font-weight: bold;
color: #00D8FF;
margin-right: 4px;
}
.unit {
font-size: 12px;
color: rgba(255, 255, 255);
}
}
.label {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
}
}
}
.funding-container {
display: flex;
justify-content: space-between;
padding: 0 10px;
margin-top: 15px;
.funding-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
position: relative;
// Add a glow effect at the bottom like in the UI
&::after {
content: '';
position: absolute;
bottom: -10px;
left: 20%;
width: 60%;
height: 10px;
background: radial-gradient(ellipse at center, rgba(0, 160, 233, 0.4) 0%, rgba(0,0,0,0) 70%);
z-index: 0;
}
.icon-wrapper {
position: relative;
z-index: 1;
.jingfei-icon {
width: 60px;
height: 60px;
object-fit: contain;
}
}
.content {
display: flex;
flex-direction: column;
z-index: 1;
background-size: 100% 100%;
background-repeat: no-repeat;
justify-content: center;
padding-left: 10px;
width: 140px;
height: 60px;
.value-row {
.value {
font-size: 20px;
font-weight: bold;
color: #00D8FF;
margin-right: 4px;
}
.unit {
font-size: 14px;
color: #fff;
}
}
.label {
font-size: 14px;
color: #fff;
margin-top: 2px;
}
}
}
}
}
}

View File

@ -1,16 +1,36 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import CommonCard from '../../UI/CommonCard'; import CommonCard from '../../UI/CommonCard';
import PerfectSystem from './components/PerfectSystem';
import SoundMechanism from './components/SoundMechanism';
import { httppost } from '@/utils/request';
import apiurl from '@/service/apiurl';
import './index.less'; import './index.less';
const SiZhi = () => { const SiZhi = () => {
const [infos, setInfos] = useState({});
const getInfo = async () => {
try {
const result = await httppost(apiurl.sq.qfg.info);
if (result.code == 200) {
const info = result.data[0];
setInfos(info);
}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getInfo();
}, []);
return ( return (
<div className="sizhi-view"> <div className="sizhi-view">
<div className="side-panel left"> <div className="side-panel left">
<CommonCard title="完善体系" className="panel-card card-1"> <CommonCard title="完善体系" className="panel-card card-1">
<div className="placeholder-content">内容填充区域</div> <PerfectSystem data={infos}/>
</CommonCard> </CommonCard>
<CommonCard title="健全机制" className="panel-card card-2"> <CommonCard title="健全机制" className="panel-card card-2">
<div className="placeholder-content">内容填充区域</div> <SoundMechanism />
</CommonCard> </CommonCard>
</div> </div>

View File

@ -4,7 +4,7 @@ import { CloseOutlined } from '@ant-design/icons';
import titleBg from '@/assets/images/modal/title.png'; import titleBg from '@/assets/images/modal/title.png';
import './index.less'; import './index.less';
const CommonModal = ({ visible, onClose, title, children, width, tabs = [], activeTab, onTabChange }) => { const CommonModal = ({ visible, onClose, title, children, width, tabs = [], activeTab, onTabChange, bodyStyle = {} }) => {
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
//当弹框打开时如果不加这行代码用户滚动鼠标滚轮时背后的页面Home 页)也会跟着滚动 //当弹框打开时如果不加这行代码用户滚动鼠标滚轮时背后的页面Home 页)也会跟着滚动
@ -45,7 +45,7 @@ const CommonModal = ({ visible, onClose, title, children, width, tabs = [], acti
<CloseOutlined /> <CloseOutlined />
</div> </div>
</div> </div>
<div className="modal-body"> <div className="modal-body" style={bodyStyle}>
{children} {children}
</div> </div>
</div> </div>

View File

@ -1,46 +1,28 @@
import React from "react"; import React from "react";
import { CloseOutlined } from '@ant-design/icons'; import CommonModal from '../CommonModal';
import { config } from "@/config";
const PdfView = ({ visible, title, onClose, url, fileId }) => { const PdfView = ({ visible, title, onClose, url, fileId }) => {
if (!visible) return null; console.log(url);
return ( return (
<div style={{ <CommonModal
position: 'fixed', visible={visible}
top: 0, title={title}
left: 0, onClose={onClose}
width: '100vw', width="60%"
height: '100vh', bodyStyle={{ padding: 0, overflow: 'hidden' }}
backgroundColor: 'rgba(0, 0, 0, 0.5)', >
zIndex: 2000, <iframe
display: 'flex', style={{
justifyContent: 'center', height: '100%',
alignItems: 'center' width: '100%',
}}> border: 0,
<div style={{ width: '60%', height: '80vh', display: 'flex', flexDirection: 'column', position: 'relative' }}> background: '#fff'
<CloseOutlined }}
onClick={onClose} src={`${process.env.PUBLIC_URL}/static/pdf/web/viewer.html?file=${encodeURIComponent(url + fileId)}`}
style={{ title={title}
position: 'absolute', />
top: '-30px', </CommonModal>
right: 0,
color: '#fff',
fontSize: '18px',
cursor: 'pointer'
}}
/>
<iframe
style={{
height: '100%',
width: '100%',
border: 0,
background: '#fff'
}}
src={`${process.env.PUBLIC_URL}/static/pdf/web/viewer.html?file=${encodeURIComponent(url + fileId)}`}
title={title}
/>
</div>
</div>
) )
} }
export default PdfView export default PdfView

View File

@ -1,22 +1,27 @@
import React from 'react'; import React from 'react';
import { Select } from 'antd'; import { DatePicker } from 'antd';
import moment from 'moment';
import './index.less'; import './index.less';
const { Option } = Select; const YearSelect = ({ value, onChange, className, style, ...props }) => {
const handleChange = (date, dateString) => {
if (onChange) {
onChange(dateString);
}
};
const YearSelect = ({ defaultValue = "2025", style, className, ...props }) => ( return (
<Select <DatePicker
defaultValue={defaultValue} picker="year"
style={{ width: 80, color: '#fff',marginRight:15,marginTop:5, ...style }} value={value ? moment(value, 'YYYY') : null}
bordered={false} onChange={handleChange}
dropdownClassName="year-select-dropdown" className={`custom-year-select ${className || ''}`}
className={`year-select ${className || ''}`} dropdownClassName="custom-year-select-dropdown"
{...props} style={style}
> allowClear={false}
<Option value="2025">2025</Option> {...props}
<Option value="2024">2024</Option> />
<Option value="2023">2023</Option> );
</Select> };
);
export default YearSelect; export default YearSelect;

View File

@ -1,23 +1,81 @@
.year-select { .custom-year-select {
color: #fff; background: transparent !important;
.ant-select-selector { border: 1px solid rgba(255, 255, 255, 0.3) !important; // Added border
color: #fff !important; border-radius: 4px; // Optional: rounded corners for better look
background-color: transparent !important; width: 90px;
input {
color: #fff !important;
font-size: 14px;
font-weight: normal; // Changed from bold to normal
cursor: pointer;
} }
.ant-select-arrow {
&:hover {
border-color: #00a0e9 !important; // Highlight border on hover
}
.ant-picker-suffix {
color: #00a0e9;
}
.ant-picker-clear {
background: transparent;
color: #fff; color: #fff;
} }
&.ant-picker-focused {
box-shadow: none;
}
} }
// Global styles for dropdown (since it renders in body) .custom-year-select-dropdown {
.year-select-dropdown { background-color: rgba(0, 40, 70, 0.95) !important;
background-color: rgba(0, 20, 50, 0.9) !important; border: 1px solid #00a0e9;
border: 1px solid rgba(0, 160, 233, 0.3);
.ant-picker-header {
.ant-select-item {
color: #fff; color: #fff;
&:hover, &.ant-select-item-option-selected { border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background-color: rgba(0, 160, 233, 0.3);
button {
color: #fff;
&:hover { color: #00a0e9; }
}
.ant-picker-header-super-prev-btn, .ant-picker-header-super-next-btn {
color: #fff;
} }
} }
}
.ant-picker-body {
color: #fff;
}
.ant-picker-content {
th, td { color: #fff; }
}
.ant-picker-cell {
color: rgba(255, 255, 255, 0.6);
&:hover .ant-picker-cell-inner {
background-color: rgba(0, 160, 233, 0.3) !important;
}
}
.ant-picker-cell-in-view {
color: #fff;
}
.ant-picker-cell-selected .ant-picker-cell-inner {
background-color: #00a0e9 !important;
color: #fff;
}
.ant-picker-year-panel {
.ant-picker-cell-inner {
color: #fff;
&:hover {
background: rgba(0, 160, 233, 0.3);
}
}
}
}