feat():开发水库水情弹框

qzc-dev
李神峰 2026-02-02 17:15:03 +08:00
parent 793319bbdc
commit d246a90eab
59 changed files with 2816 additions and 76 deletions

View File

@ -26,6 +26,7 @@
"crypto-js": "^4.1.1",
"echarts": "^4.9.0",
"echarts-for-react": "3.0.2",
"ezuikit-js": "8.0.4-beta.1",
"http-proxy-middleware": "^2.0.6",
"moment": "^2.29.4",
"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: 6.2 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

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

@ -10,6 +10,20 @@ const apiurl = {
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: {
qfg: {
@ -36,7 +50,13 @@ const apiurl = {
nearbyHistory:service + '/attResBase/maxRain' //获取历史近几小时数据
},
reservoir: {
list:service + '/screen/monitoring/rsvr'
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: {

View File

@ -35,3 +35,20 @@ export async function reservoirlist(params) {
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

@ -8,12 +8,28 @@ 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: '站名',
@ -46,10 +62,10 @@ const AllWeatherControl = () => {
// Reservoir Data
const reservoirData = [
{ label: '主坝坝前', value: reservoirItem?.rz, unit: 'm', upArrow: reservoirItem?.status > 0, underline: true,downArrow:reservoirItem?.status < 0 },
{ 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' },
{ label: '副坝坝前', value: reservoirItem?.rz, unit: 'm', clickable: true,underline: true },
{ label: '当前库容', value: reservoirItem?.nowCap, unit: '万m³' },
{ label: '有效库容', value: reservoirItem?.effectiveCap, unit: '万m³' },
];
@ -80,6 +96,11 @@ const AllWeatherControl = () => {
getReservoir()
}, [])
const openUavModal = (key) => {
setUavActiveTab(key);
setUavModalVisible(true);
}
return (
<div className="all-weather-control">
{/* 雨情 Section */}
@ -111,7 +132,6 @@ const AllWeatherControl = () => {
visible={detailVisible}
onClose={() => setDetailVisible(false)}
width={'70%'}
bodyStyle={{ background: 'transparent', padding: 12 }}
>
<RightPanel stcd={selectedStcd?.stcd} cleanMode={true} />
</CommonModal>
@ -130,7 +150,12 @@ const AllWeatherControl = () => {
<div
key={index}
className="reservoir-card"
style={{ backgroundImage: `url(${smallCard})` }}
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>
@ -142,6 +167,14 @@ const AllWeatherControl = () => {
</div>
))}
</div>
<CommonModal
title={reservoirItem?.stnm || '水库水情'}
visible={reservoirVisible}
onClose={() => setReservoirVisible(false)}
width={'90%'}
>
<ReservoirPanel stcd={reservoirItem?.stcd} cleanMode={true} />
</CommonModal>
</div>
{/* 无人机 Section */}
@ -151,7 +184,7 @@ const AllWeatherControl = () => {
<img src={arrowIcon} alt="arrow" className="arrow-icon" />
<span className="section-title">无人机</span>
</div>
<span className="link">视频墙</span>
<span className="link" onClick={()=>setVideoOpen(true)}>视频墙</span>
</div>
<div className="uav-content">
<div
@ -159,10 +192,29 @@ const AllWeatherControl = () => {
style={{ backgroundImage: `url(${wrj})` }}
/>
<div className="uav-actions">
<div className="uav-button">直播画面</div>
<div className="uav-button">巡查任务</div>
<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>
);

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

@ -49,8 +49,7 @@ export default function DrpOption({ echartData, grid }) {
},
axisLine: {
lineStyle: {
color: '#07a6ff',
width: 0.5,
color: 'rgba(255,255,255,0.5)',
}
},
axisTick: {
@ -67,8 +66,7 @@ export default function DrpOption({ echartData, grid }) {
splitLine: {
show: true,
lineStyle: {
color: '#07a6ff',
width: 0.25,
color: 'rgba(255,255,255,0.5)',
type: 'dashed'
}
},
@ -93,8 +91,7 @@ export default function DrpOption({ echartData, grid }) {
splitLine: {
show: true,
lineStyle: {
color: '#07a6ff',
width: 0.25,
color: 'rgba(255,255,255,0.5)',
type: 'dashed'
}
},

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

@ -6,68 +6,10 @@ 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 ReservoirPanel = () => {
const columns = [
{ title: '时间', dataIndex: 'tm', key: 'tm', width: 140, align: 'center' },
{ title: '水位(m)', dataIndex: 'rz', key: 'rz', align: 'center' },
{ title: '库容(万m³)', dataIndex: 'w', key: 'w', align: 'center' },
];
const data = [];
const option = {
title: { text: '水位趋势', left: 'center', textStyle: { color: '#fff' } },
grid: { left: '8%', right: '4%', bottom: '10%', top: '15%' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: data.map(i => i.tm), axisLabel: { color: '#fff' } },
yAxis: { type: 'value', axisLabel: { color: '#fff' }, splitLine: { show: true, lineStyle: { color: 'rgba(255,255,255,0.2)' } } },
series: [{ type: 'line', smooth: true, data: data.map(i => i.rz), lineStyle: { color: '#00a0e9' } }],
};
return (
<div className="awm-grid">
<div className="awm-left">
<Table columns={columns} dataSource={data} size="small" pagination={false} scroll={{ y: 360 }} />
</div>
<div className="awm-right">
<ReactEcharts option={option} style={{ height: '100%', width: '100%' }} />
</div>
</div>
);
};
const FlowPanel = () => {
const columns = [
{ title: '时间', dataIndex: 'tm', key: 'tm', width: 140, align: 'center' },
{ title: '入库(m³/s)', dataIndex: 'qin', key: 'qin', align: 'center' },
{ title: '出库(m³/s)', dataIndex: 'qout', key: 'qout', align: 'center' },
];
const data = [];
const option = {
title: { text: '出入库流量', left: 'center', textStyle: { color: '#fff' } },
grid: { left: '8%', right: '4%', bottom: '10%', top: '15%' },
tooltip: { trigger: 'axis' },
legend: { data: ['入库', '出库'], textStyle: { color: '#fff' } },
xAxis: { type: 'category', data: data.map(i => i.tm), axisLabel: { color: '#fff' } },
yAxis: { type: 'value', axisLabel: { color: '#fff' }, splitLine: { show: true, lineStyle: { color: 'rgba(255,255,255,0.2)' } } },
series: [
{ name: '入库', type: 'line', data: data.map(i => i.qin), lineStyle: { color: '#00a0e9' } },
{ name: '出库', type: 'line', data: data.map(i => i.qout), lineStyle: { color: '#3b7cff' } },
],
};
return (
<div className="awm-grid">
<div className="awm-left">
<Table columns={columns} dataSource={data} size="small" pagination={false} scroll={{ y: 360 }} />
</div>
<div className="awm-right">
<ReactEcharts option={option} style={{ height: '100%', width: '100%' }} />
</div>
</div>
);
};
const SafetyPanel = () => {
return (
<div className="awm-empty">内容待接入</div>

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

@ -117,7 +117,7 @@ const SiQuan = () => {
tabs={modalType === 'monitor' ? tabs : (modalType === 'allweather' ? tabsAllWeather : [])}
activeTab={activeTab}
onTabChange={handleTabChange}
width={modalType === 'cycle' ? '70%': undefined}
width={modalType === 'cycle' ? '70%':modalType === 'allweather' ? '90%': undefined}
>
{/* Content changes based on activeTab */}
<div style={{color: '#fff', height: '100%' }}>

View File

@ -6,7 +6,7 @@
input {
color: #fff !important;
font-size: 16px;
font-size: 14px;
font-weight: normal; // Changed from bold to normal
cursor: pointer;
}