620 lines
25 KiB
TypeScript
620 lines
25 KiB
TypeScript
import { Col, Descriptions, Radio, Row, Table } from 'antd';
|
||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||
import './style.less';
|
||
import './china'
|
||
import * as echarts from 'echarts';
|
||
import warn_icon_01 from '@/assets/screen/warn_icon_01.png'
|
||
import warn_icon_02 from '@/assets/screen/warn_icon_02.png'
|
||
import crown_01 from '@/assets/screen/crown_01.png'
|
||
import crown_02 from '@/assets/screen/crown_02.png'
|
||
import crown_03 from '@/assets/screen/crown_03.png'
|
||
import moment from 'moment';
|
||
import { getApplicationData, getMonitorSample, getRoomProjectData, getTodayExpert, getTotalMapData, getWarnData } from './service';
|
||
import { debounce } from 'lodash';
|
||
import { RightCircleOutlined } from '@ant-design/icons';
|
||
import { history } from 'umi';
|
||
import ScreenVideoPlay from '@/components/Screen/ScreenVideoPlay';
|
||
|
||
export const onCell = (_: any, rowIndex: any) => ({ className: rowIndex % 2 == 0 ? "screen-table-odd-content" : "screen-table-even-content", });
|
||
export const onHeaderCell = () => ({ className: "screen-table-header", });
|
||
export const roomStatusMap = ["已预约", "评标中", "评标结束"];
|
||
const method = ["公开比选", "公开询价", "公开招募", "竞争性谈判", "单一来源"]
|
||
const zeroProvince = ["香港", "澳门", "台湾", "南海诸岛"];
|
||
export const proviceEnum = {
|
||
"0011": "北京",
|
||
"0012": "天津",
|
||
"0013": "河北",
|
||
"0014": "山西",
|
||
"0015": "内蒙古",
|
||
"0021": "辽宁",
|
||
"0022": "吉林",
|
||
"0023": "黑龙江",
|
||
"0031": "上海",
|
||
"0032": "江苏",
|
||
"0033": "浙江",
|
||
"0034": "安徽",
|
||
"0035": "福建",
|
||
"0036": "江西",
|
||
"0037": "山东",
|
||
"0041": "河南",
|
||
"0042": "湖北",
|
||
"0043": "湖南",
|
||
"0044": "广东",
|
||
"0045": "广西",
|
||
"0046": "海南",
|
||
"0050": "重庆",
|
||
"0051": "四川",
|
||
"0052": "贵州",
|
||
"0053": "云南",
|
||
"0054": "西藏",
|
||
"0061": "陕西",
|
||
"0062": "甘肃",
|
||
"0063": "青海",
|
||
"0064": "宁夏",
|
||
"0065": "新疆",
|
||
"001000": "集团"
|
||
}
|
||
|
||
const evalColumn: any[] = [
|
||
{
|
||
title: '省分',
|
||
dataIndex: 'provinceDictId',
|
||
key: 'provinceDictId',
|
||
align: 'center',
|
||
ellipsis: true,
|
||
onCell,
|
||
onHeaderCell,
|
||
render: (_: any, record: any) => proviceEnum[_],
|
||
},
|
||
{
|
||
title: '项目名称',
|
||
dataIndex: 'projectName',
|
||
key: 'projectName',
|
||
ellipsis: true,
|
||
onCell,
|
||
onHeaderCell,
|
||
},
|
||
{
|
||
title: '专家人数',
|
||
dataIndex: 'userNumber',
|
||
key: 'userNumber',
|
||
align: 'center',
|
||
onCell,
|
||
onHeaderCell,
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
align: 'center',
|
||
onCell,
|
||
onHeaderCell,
|
||
render: (_: any, record: any) => roomStatusMap[_],
|
||
},
|
||
];
|
||
/**
|
||
* 当前时间
|
||
* @returns
|
||
*/
|
||
export const LocalTime = () => {
|
||
const [time, setTime] = useState<string>('');
|
||
useEffect(() => {
|
||
const interval = setInterval(function () {
|
||
const date = moment().format("YYYY-MM-DD HH:mm:ss");
|
||
setTime(date);
|
||
}, 1000);
|
||
return () => {
|
||
clearInterval(interval)
|
||
};
|
||
}, [])
|
||
return (
|
||
<span>当前时间:{time}</span>
|
||
)
|
||
}
|
||
|
||
const projectClick = () => {
|
||
history.push("/MonitorScreen/MonitorRoom");
|
||
}
|
||
const GraphChart = (props: { type: string, chartData: any[] }) => {
|
||
const { type, chartData } = props;
|
||
const random = Math.random().toString();
|
||
useEffect(() => {
|
||
type EChartsOption = echarts.EChartsOption;
|
||
const chartDom = document.getElementById(random)!;
|
||
const myChart = echarts.init(chartDom);
|
||
const categoryOption: EChartsOption = {
|
||
legend: { top: '3%', textStyle: { color: '#fff' } },
|
||
tooltip: {},
|
||
grid: { left: '8%', right: '4%', top: '16%', bottom: 60 },
|
||
dataset: {
|
||
source: chartData
|
||
},
|
||
xAxis: { type: 'category', axisLabel: { interval: 0, color: '#fff' } },
|
||
yAxis: { axisLabel: { color: '#fff' } },
|
||
// Declare several bar series, each will be mapped
|
||
// to a column of dataset.source by default.
|
||
series: [{ type: 'bar' }, { type: 'bar' }]
|
||
};
|
||
const pieOption: EChartsOption = {
|
||
legend: {
|
||
orient: 'vertical',
|
||
right: '3%',
|
||
top: 'middle',
|
||
textStyle: { color: '#fff' }
|
||
},
|
||
tooltip: {},
|
||
dataset: {
|
||
source: chartData
|
||
},
|
||
series: [
|
||
{
|
||
type: 'pie',
|
||
radius: ['40%', '70%'],
|
||
center: ['15%', '50%'],
|
||
label: {
|
||
show: false,
|
||
position: 'center'
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: true,
|
||
fontSize: '20px',
|
||
fontWeight: 'bold',
|
||
color: '#fff'
|
||
}
|
||
},
|
||
encode: {
|
||
itemName: 'product',
|
||
value: 'reserve'
|
||
}
|
||
},
|
||
{
|
||
type: 'pie',
|
||
radius: ['40%', '70%'],
|
||
center: ['45%', '50%'],
|
||
label: {
|
||
show: false,
|
||
position: 'center'
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: true,
|
||
fontSize: '20px',
|
||
fontWeight: 'bold',
|
||
color: '#fff'
|
||
}
|
||
},
|
||
encode: {
|
||
itemName: 'product',
|
||
value: 'ing'
|
||
}
|
||
},
|
||
{
|
||
type: 'pie',
|
||
radius: ['40%', '70%'],
|
||
center: ['75%', '50%'],
|
||
label: {
|
||
show: false,
|
||
position: 'center'
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: true,
|
||
fontSize: '20px',
|
||
fontWeight: 'bold',
|
||
color: '#fff'
|
||
}
|
||
},
|
||
encode: {
|
||
itemName: 'product',
|
||
value: 'end'
|
||
}
|
||
}
|
||
]
|
||
};
|
||
const mapOption: EChartsOption = {
|
||
tooltip: {
|
||
trigger: 'item',
|
||
showDelay: 0,
|
||
transitionDuration: 0.2,
|
||
padding: [8, 12],
|
||
formatter: (params: any) => {
|
||
return `<div style="display:flex;justify-content:flex-start;align-items:center;font-weight:600">
|
||
<div style="width:8px;height:8px;border-radius:8px;background:#FFCC00;margin-right:8px"></div>${params.data?.name}
|
||
</div>
|
||
<div style="margin-top:8px;margin-left:16px;">
|
||
评标室间数:${params.data?.placeNumber}
|
||
</div>
|
||
<div style="margin-top:8px;margin-left:16px;">
|
||
正在评标:${params.data?.openingNumber}
|
||
</div>
|
||
<div style="margin-top:8px;margin-left:16px;">
|
||
评标专家:${params.data?.expertNumber}
|
||
</div>
|
||
<div style="margin-top:8px;margin-left:16px;">
|
||
累计评标次数:${params.data?.cumulativePlaceNumber}
|
||
</div>`
|
||
}
|
||
},
|
||
visualMap: {
|
||
show: false,
|
||
min: 0,
|
||
max: 1000,
|
||
inRange: {
|
||
color: [
|
||
'#ECF3F4',
|
||
'#E3F4FF',
|
||
'#D1ECFD',
|
||
'#A0D6F8',
|
||
'#5DCAF2',
|
||
'#09A6F7',
|
||
'#0081C4',
|
||
'#26519E',
|
||
'#052D74',
|
||
'#001F53',
|
||
]
|
||
},
|
||
},
|
||
series: [
|
||
{
|
||
name: '累计评标次数',
|
||
type: 'map',
|
||
roam: false,
|
||
zoom: 1.2,
|
||
left: "20%",
|
||
map: 'china',
|
||
emphasis: {
|
||
label: {
|
||
show: false,
|
||
},
|
||
itemStyle: {
|
||
areaColor: '#FFCC00'
|
||
}
|
||
},
|
||
select: {
|
||
disabled: true,
|
||
},
|
||
data: chartData,
|
||
}
|
||
]
|
||
};
|
||
|
||
const option: EChartsOption = type == "pie" ? pieOption : type == "map" ? mapOption : categoryOption;
|
||
myChart.setOption(option);
|
||
const resize = () => {
|
||
myChart && myChart.resize();
|
||
};
|
||
window.addEventListener("resize", debounce(() => resize(), 100));
|
||
return () => {
|
||
window.removeEventListener("resize", debounce(() => resize(), 100));
|
||
}
|
||
}, [chartData])
|
||
return (
|
||
<div id={random} style={{ width: '100%', height: '100%', zIndex: 99 }}></div>
|
||
)
|
||
}
|
||
|
||
//异常预警
|
||
export const EarlyWarn = ({ img, name, num }: { img: string, name: string, num: string | number }) => {
|
||
return (
|
||
<div className='screen-warn-content'>
|
||
<img src={img} className='screen-warn-img' />
|
||
<div className='screen-warn-content'>
|
||
<div className='screen-warn-num'>{num}</div>
|
||
<div>{name}</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default () => {
|
||
//中心地图及周边数据
|
||
const [centerMapData, setCenterMapData] = useState<any>();
|
||
//异常预警数据
|
||
const [earlyWarnData, setEarlyWarnData] = useState<any>();
|
||
//评标室项目数据
|
||
const [bidProjectData, setBidProjectData] = useState<any[]>([]);
|
||
//今日评标专家数量数据
|
||
const [todayExpertNum, setTodayExpertNum] = useState<any[]>([]);
|
||
//评标室应用情况数据
|
||
const [evalApplData, setEvalApplData] = useState<any[]>([]);
|
||
//当前播放的设备参数
|
||
const [cameraParams, setCameraParams] = useState<any>();
|
||
//当月&年转换
|
||
const [radioSelect, setRadioSelect] = useState<string>("1");
|
||
//评标室监控示例数据
|
||
const [sampleData, setSampleData] = useState<any>();
|
||
//监控视频Ref
|
||
const videoRef = useRef<any>();
|
||
//定时刷新间隔
|
||
const _time = 10000;
|
||
const categoryChart = useMemo(() => {
|
||
return todayExpertNum.length > 0 && <GraphChart type="category" chartData={todayExpertNum} />
|
||
}, [todayExpertNum])
|
||
const pieChart = useMemo(() => {
|
||
return evalApplData.length > 0 && <GraphChart type="pie" chartData={evalApplData} />
|
||
}, [evalApplData])
|
||
const mapChart = useMemo(() => {
|
||
return centerMapData?.list && <GraphChart type="map" chartData={centerMapData?.list} />
|
||
}, [centerMapData])
|
||
//星期&年切换
|
||
const onRadioChange = (e: any) => {
|
||
const type = e.target.value;
|
||
setRadioSelect(type);
|
||
getEvalApplData(type);
|
||
}
|
||
//获取中间地图数据
|
||
const getMapList = () => {
|
||
getTotalMapData({ pageNo: 1, pageSize: 10 }).then(res => {
|
||
if (res?.code == 200) {
|
||
const data = res?.data;
|
||
for (const ite of data.list) {
|
||
ite["name"] = ite.provinceDictName;
|
||
ite["value"] = ite.placeNumber;
|
||
}
|
||
for (const name of zeroProvince) {//有些省市字典没有,默认放0
|
||
data.list.push({ name: name, value: 0, cumulativePlaceNumber: 0, expertNumber: 0, openingNumber: 0, placeNumber: 0 });
|
||
}
|
||
setCenterMapData(data);
|
||
}
|
||
})
|
||
}
|
||
//获取异常预警数据
|
||
const getWarnInfo = () => {
|
||
getWarnData().then(res => {
|
||
if (res?.code == 200) {
|
||
const data = res?.data;
|
||
setEarlyWarnData(data);
|
||
}
|
||
})
|
||
}
|
||
//获取评标室项目情况
|
||
const getEvaRoomData = () => {
|
||
getRoomProjectData({ pageNo: 1, pageSize: 20 }).then(res => {
|
||
if (res?.code == 200) {
|
||
const data = res?.data;
|
||
setBidProjectData(data);
|
||
}
|
||
})
|
||
}
|
||
//获取今日评标专家数量
|
||
const getExpertNumber = () => {
|
||
getTodayExpert().then(res => {
|
||
if (res?.code == 200) {
|
||
const data = res?.data;
|
||
let formatData = [['product', '专家人数', '专家签到数量'], ['招标项目', data[0].number + data[1].number, data[0].signNumber + data[1].signNumber]];
|
||
for (const ite of data) {
|
||
if (method.includes(ite.bidMethod)) {
|
||
formatData.push([ite.bidMethod, ite.number, ite.signNumber]);
|
||
}
|
||
}
|
||
setTodayExpertNum(formatData);
|
||
}
|
||
})
|
||
}
|
||
//获取评标室应用情况
|
||
const getEvalApplData = (type: string) => {
|
||
getApplicationData({ type }).then(res => {
|
||
if (res?.code == 200) {
|
||
const data = res?.data;
|
||
let formatData = [['product', 'reserve', 'ing', 'end'], ['招标项目', String(Number(data.reserveNumber[0].number) + Number(data.reserveNumber[1].number)), String(Number(data.ingNumber[0].number) + Number(data.ingNumber[1].number)), String(Number(data.endNumber[0].number) + Number(data.endNumber[1].number))]];
|
||
let comp = ['公开比选'];
|
||
let inqu = ['公开询价'];
|
||
let rect = ['公开招募'];
|
||
let nego = ['竞争性谈判'];
|
||
let only = ['单一来源'];
|
||
for (const ite of data.reserveNumber) {
|
||
if (ite.bidMethod == method[0]) {
|
||
comp.push(ite.number)
|
||
} else if (ite.bidMethod == method[1]) {
|
||
inqu.push(ite.number)
|
||
} else if (ite.bidMethod == method[2]) {
|
||
rect.push(ite.number)
|
||
} else if (ite.bidMethod == method[3]) {
|
||
nego.push(ite.number)
|
||
} else if (ite.bidMethod == method[4]) {
|
||
only.push(ite.number)
|
||
}
|
||
}
|
||
for (const ite of data.ingNumber) {
|
||
if (ite.bidMethod == method[0]) {
|
||
comp.push(ite.number)
|
||
} else if (ite.bidMethod == method[1]) {
|
||
inqu.push(ite.number)
|
||
} else if (ite.bidMethod == method[2]) {
|
||
rect.push(ite.number)
|
||
} else if (ite.bidMethod == method[3]) {
|
||
nego.push(ite.number)
|
||
} else if (ite.bidMethod == method[4]) {
|
||
only.push(ite.number)
|
||
}
|
||
}
|
||
for (const ite of data.endNumber) {
|
||
if (ite.bidMethod == method[0]) {
|
||
comp.push(ite.number)
|
||
} else if (ite.bidMethod == method[1]) {
|
||
inqu.push(ite.number)
|
||
} else if (ite.bidMethod == method[2]) {
|
||
rect.push(ite.number)
|
||
} else if (ite.bidMethod == method[3]) {
|
||
nego.push(ite.number)
|
||
} else if (ite.bidMethod == method[4]) {
|
||
only.push(ite.number)
|
||
}
|
||
}
|
||
formatData.push(comp);
|
||
formatData.push(inqu);
|
||
formatData.push(rect);
|
||
formatData.push(nego);
|
||
formatData.push(only);
|
||
setEvalApplData(formatData);
|
||
}
|
||
})
|
||
}
|
||
//获取首页评标室监控示例
|
||
const getSampleData = () => {
|
||
getMonitorSample().then(res => {
|
||
if (res?.code == 200) {
|
||
const data = res?.data;
|
||
if (data) {
|
||
setSampleData(data);
|
||
setCameraParams(data?.devicePageVOList[0].platform);
|
||
setTimeout(() => {
|
||
videoRef.current?.play(data?.devicePageVOList[0].deviceCode);
|
||
}, 4000);
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
useEffect(() => {
|
||
getMapList();
|
||
getWarnInfo();
|
||
getEvaRoomData();
|
||
getExpertNumber();
|
||
getEvalApplData("1");
|
||
getSampleData();
|
||
}, [])
|
||
|
||
//定时器
|
||
useEffect(() => {
|
||
const interval = setInterval(function () {
|
||
getWarnInfo();
|
||
getEvaRoomData();
|
||
}, _time);
|
||
return () => {
|
||
clearInterval(interval)
|
||
};
|
||
}, [])
|
||
|
||
return (
|
||
<div className="screen-bg">
|
||
<div className='top-block'>
|
||
</div>
|
||
<div className='top'>
|
||
<Row>
|
||
<Col span={6}>
|
||
<div className='top-left'>
|
||
</div>
|
||
</Col>
|
||
<Col span={12}>
|
||
<div className='top-title'>
|
||
<span>数字化评标监控平台</span>
|
||
</div>
|
||
</Col>
|
||
<Col span={6}>
|
||
<div className='top-right'>
|
||
<LocalTime />
|
||
</div>
|
||
</Col>
|
||
</Row>
|
||
</div>
|
||
<Row className="screen-content">
|
||
<Col span={18}>
|
||
<Row>
|
||
<Col span={8} className='screen-right4'>
|
||
<div className='screen-card'>
|
||
<div className='card-title'>
|
||
<span>今日评标专家数量</span>
|
||
</div>
|
||
{categoryChart}
|
||
</div>
|
||
<div className='screen-card screen-top8'>
|
||
<div className='card-title'>
|
||
<span>评标室监控</span>
|
||
</div>
|
||
{
|
||
sampleData && <>
|
||
<div className='card-project-content'>
|
||
<p>项目名称:{sampleData?.projectName}</p>
|
||
<p>标段名称:{sampleData?.sectionName}</p>
|
||
<p>监控设备名称:{sampleData?.devicePageVOList[0].deviceName}</p>
|
||
<p><span>评标室名称:{sampleData?.areaName}</span><span>省分:北京</span></p>
|
||
</div>
|
||
<div className='card-carema-c'>
|
||
{cameraParams && <ScreenVideoPlay videoRef={videoRef} cameraParams={cameraParams} status={0} />}
|
||
</div>
|
||
</>
|
||
}
|
||
</div>
|
||
</Col>
|
||
<Col span={16} className='screen-left4 screen-right4'>
|
||
<div className='middle-c'>
|
||
<div className='map-bg'>
|
||
{mapChart}
|
||
<div className='map-total-num'>
|
||
<span>当前全国评标数量:{centerMapData?.number}</span>
|
||
</div>
|
||
<div className='map-grand'>
|
||
<div className='map-grand-title'>累计评标</div>
|
||
<div>
|
||
<img src={crown_01} className='map-grand-img' />
|
||
<span>{centerMapData?.list?.[0].name}:{centerMapData?.list?.[0].cumulativePlaceNumber}</span>
|
||
</div>
|
||
<div className='map-grand-content'>
|
||
<img src={crown_02} className='map-grand-img' />
|
||
<span>{centerMapData?.list?.[1].name}:{centerMapData?.list?.[1].cumulativePlaceNumber}</span>
|
||
</div>
|
||
<div className='map-grand-content'>
|
||
<img src={crown_03} className='map-grand-img' />
|
||
<span>{centerMapData?.list?.[2].name}:{centerMapData?.list?.[2].cumulativePlaceNumber}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Col>
|
||
<Col span={24} className='screen-top8 screen-right4'>
|
||
<div className='screen-card'>
|
||
<div className='screen-graph-top'>
|
||
<div className='screen-graph-left-title'>
|
||
评标室应用情况统计
|
||
</div>
|
||
{evalApplData.length > 0 && <Radio.Group buttonStyle="solid" size='small' value={radioSelect} onChange={onRadioChange}>
|
||
<Radio.Button value="1">当月</Radio.Button>
|
||
<Radio.Button value="2"> 年 </Radio.Button>
|
||
</Radio.Group>}
|
||
</div>
|
||
<div className='screen-graph-chart'>
|
||
{pieChart}
|
||
</div>
|
||
<div className='screen-graph-bottom'>
|
||
<span className='screen-graph-title'>已预约</span>
|
||
<span className='screen-graph-title'>评审中</span>
|
||
<span className='screen-graph-title'>评审结束</span>
|
||
</div>
|
||
{/* <div className='screen-graph-end'>
|
||
<span>>>项目列表</span>
|
||
</div> */}
|
||
</div>
|
||
</Col>
|
||
</Row>
|
||
</Col>
|
||
<Col span={6} className='screen-left4'>
|
||
<div className='screen-card'>
|
||
<div className='card-title'>
|
||
<span>异常预警</span>
|
||
</div>
|
||
<div className='screen-warn'>
|
||
<EarlyWarn name="陌生人预警" img={warn_icon_01} num={earlyWarnData?.strangerCount ? earlyWarnData?.strangerCount : 0} />
|
||
<EarlyWarn name="评标室人数预警" img={warn_icon_02} num={earlyWarnData?.numberCount ? earlyWarnData?.numberCount : 0} />
|
||
</div>
|
||
</div>
|
||
<div className='screen-card screen-card-double screen-top8'>
|
||
<div className='card-title'>
|
||
<span>评标室项目情况<RightCircleOutlined style={{ marginLeft: "0.5rem", cursor: 'pointer' }} onClick={() => projectClick()} /></span>
|
||
</div>
|
||
{bidProjectData.length > 0 && <Table
|
||
pagination={false}
|
||
className="screen-table"
|
||
rowKey="id"
|
||
size="small"
|
||
dataSource={bidProjectData}
|
||
columns={evalColumn}
|
||
/>}
|
||
</div>
|
||
</Col>
|
||
</Row>
|
||
</div>
|
||
);
|
||
};;
|