feat():开发水库水情弹框
|
|
@ -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",
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 567 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 199 B |
|
After Width: | Height: | Size: 143 B |
|
After Width: | Height: | Size: 259 B |
|
After Width: | Height: | Size: 651 B |
|
After Width: | Height: | Size: 269 B |
|
After Width: | Height: | Size: 157 B |
|
After Width: | Height: | Size: 226 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 1014 B |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 619 B |
|
Before Width: | Height: | Size: 589 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 646 B |
|
After Width: | Height: | Size: 748 B |
|
After Width: | Height: | Size: 664 B |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 964 B |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 604 B |
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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*1,2代表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': '对讲不支持这种音频编码格式',
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -33,5 +33,22 @@ export async function reservoirlist(params) {
|
|||
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||[];
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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">最大瞬时流量(m³/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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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%;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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%' }}>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
input {
|
||||
color: #fff !important;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: normal; // Changed from bold to normal
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||