import React, { useContext, useState, useReducer, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Highcharts from 'highcharts/highstock'; import HighchartsReact from 'highcharts-react-official'; import { Tabs, Select, Radio, Checkbox, ConfigProvider, DatePicker } from 'antd'; import { PlusCircleOutlined } from '@ant-design/icons'; import TimeRangePicker from '@wisdom-components/timerangepicker'; import BasicTable from '@wisdom-components/basictable'; import Empty from '@wisdom-components/empty'; import moment from 'moment'; import './index.less'; const { TabPane } = Tabs; const { RangePicker } = DatePicker; const { Option } = Select; const UPDATE_TIME = { UPDATE_TIME: 'updateTime', UPDATE_BATCH_TIME: 'updateBatchTime', UPDATE_DATA_THIN: 'updateDataThin', }; const reducer = (state, action) => { switch (action.type) { case UPDATE_TIME.UPDATE_TIME: return { ...state, dateRange: updateTime(action.payload), }; case UPDATE_TIME.UPDATE_BATCH_TIME: return { ...state, dateRange: action.payload, }; case 'updateIgnoreOutliers': return { ...state, ignoreOutliers: action.payload, }; case UPDATE_TIME.UPDATE_DATA_THIN: const { zoom, unit } = action.payload; return { ...state, zoom, unit, }; default: throw new Error(); } }; const updateTime = (key) => { let start = '', end = ''; if (Array.isArray(key)) { start = moment(key[0]).format(timeFormat); end = moment(key[1]).format(timeFormat); } else { switch (key) { case 'oneHour': start = moment().subtract(1, 'hour').format(timeFormat); end = moment().format(timeFormat); break; case 'fourHour': start = moment().subtract(4, 'hour').format(timeFormat); end = moment().format(timeFormat); break; case 'twelveHours': start = moment().subtract(12, 'hour').format(timeFormat); end = moment().format(timeFormat); break; case 'roundClock': start = moment().subtract(24, 'hour').format(timeFormat); end = moment().format(timeFormat); break; case 'yesterday': start = moment().subtract(1, 'days').format(startFormat); end = moment().subtract(1, 'days').format(endFormat); break; } } return [ { dateFrom: start, dateTo: end, }, ]; }; const unique = (arr) => { let unique = {}; arr.forEach((item) => { unique[JSON.stringify(item)] = item; }); arr = Object.keys(unique).map((v) => { return JSON.parse(v); }); return arr; }; let chartWidth = 0; // chart width let chartHeight = 0; // chart height const HistoryInfo = (props) => { const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); const prefixCls = getPrefixCls('history-info'); const { title, columns, dataSource, tableProps, chartOptions, onChange } = props; const [activeTabKey, setActiveTabKey] = useState('curve'); const [timeValue, setTimeValue] = useState('customer'); const [contrastOption, setContrastOption] = useState('day'); const [customerChecked, setCustomerChecked] = useState(props.defaultChecked || 'oneHour'); const [customerTime, setCustomerTime] = useState(null); const [datePickerArr, setDatePickerArr] = useState(DataPickerArr); const [checkboxData, setCheckboxData] = useState(CheckboxData); const [dataThinKey, setDataThinKey] = useState(timeIntervalList[0].key); const [options, setOptions] = useState({}); const [state, dispatch] = useReducer(reducer, initialState); const container = useRef(null); useEffect(() => { onChange(state); }, [state]); useEffect(() => { dispatch({ type: UPDATE_TIME.UPDATE_TIME, payload: props.defaultChecked }); }, [props.defaultChecked]); useEffect(() => { setOptions({ ...handleChartOptions() }); }, [chartOptions]); // 时间设置切换(自定义/同期对比) const onTimeSetChange = (e) => { setTimeValue(e.target.value); }; // 选择(日/月) const onContrastChange = (value) => { setContrastOption(value); handleBatchTime([...datePickerArr], value); }; const onCustomerRangeChange = (value) => { setCustomerTime(value); dispatch({ type: UPDATE_TIME.UPDATE_TIME, payload: value }); }; const onCustomerTimeChange = (key) => { setCustomerChecked(key); dispatch({ type: UPDATE_TIME.UPDATE_TIME, payload: key }); }; const handleBatchTime = (arr, contrastOption) => { let newArr = []; arr.forEach((child) => { if (child.value) { newArr.push({ dateFrom: moment(child.value).startOf(contrastOption).format(startFormat), dateTo: moment(child.value).endOf(contrastOption).format(endFormat), }); } }); newArr = unique(newArr); dispatch({ type: UPDATE_TIME.UPDATE_BATCH_TIME, payload: newArr }); }; const onContrastPickerChange = (date, dateString, item) => { let arr = [...datePickerArr]; arr.forEach((child) => { if (child.key === item.key) { child.value = date; } }); handleBatchTime(arr, contrastOption); }; //新增日期选择组件 const handleAddDatePicker = () => { setDatePickerArr([ ...datePickerArr, { key: datePickerArr.length + 1, value: '', }, ]); }; const onCheckboxChange = (e, key) => { let data = [...checkboxData]; data.forEach((item) => { if (item.key === key) { item.checked = e.target.checked; if (key === 'dataThin') { if (e.target.checked) { timeIntervalList.forEach((child) => { if (child.key === dataThinKey) { dispatch({ type: item.type, payload: { zoom: dataThinKey, unit: child.unit } }); } }); } else { dispatch({ type: item.type, payload: { zoom: '', unit: '' } }); } } if (key === 'ignoreOutliers') { dispatch({ type: item.type, payload: e.target.checked }); } if (key === 'curveCenter') { setOptions({ ...handleChartOptions() }); } } }); setCheckboxData(data); }; // 数据抽稀时间间隔 const onTimeIntervalChange = (value, { unit }) => { let data = checkboxData.filter((item) => item.key === 'dataThin'); if (data[0].checked) { dispatch({ type: UPDATE_TIME.UPDATE_DATA_THIN, payload: { zoom: value, unit: unit } }); } setDataThinKey(value); }; const renderCheckbox = (child) => ( <Checkbox value={child.key} checked={child.checked} onChange={(e) => onCheckboxChange(e, child.key)} > {child.label} </Checkbox> ); const renderOptions = (item) => { return ( <> <div className={classNames(`${prefixCls}-time`)}> <div className={classNames(`${prefixCls}-label`)}>时间</div> <Radio.Group defaultValue={timeValue} onChange={onTimeSetChange}> <Radio.Button value="customer">自定义</Radio.Button> <Radio.Button value="contrast">同期对比</Radio.Button> </Radio.Group> {timeValue === 'customer' && ( // 自定义 <> <TimeRangePicker onChange={onCustomerTimeChange} value={customerChecked} dataSource={timeList} /> <RangePicker className={classNames(`${prefixCls}-customer`)} onChange={onCustomerRangeChange} value={customerTime} showTime /> </> )} {timeValue === 'contrast' && ( // 同期对比 <> <Select value={contrastOption} style={{ width: 60 }} onChange={onContrastChange}> <Option value="day">日</Option> <Option value="month">月</Option> </Select> {datePickerArr.map((child, index) => ( <div key={child.key} className={classNames(`${prefixCls}-contrast-list`)}> <div className={classNames(`${prefixCls}-connect`, { first: child.key === 1 })}> 与 </div> <DatePicker picker={contrastOption} value={child.value} onChange={(date, dateString) => onContrastPickerChange(date, dateString, child)} /> </div> ))} {datePickerArr.length < 5 && <PlusCircleOutlined onClick={handleAddDatePicker} />} </> )} </div> <div className={classNames(`${prefixCls}-cover`)}> <div className={classNames(`${prefixCls}-label`)}>曲线设置</div> {checkboxData.map((child) => ( <div key={child.key}> {item.key === 'curve' && renderCheckbox(child)} {item.key === 'table' && child.key !== 'curveCenter' && renderCheckbox(child)} </div> ))} <Select value={dataThinKey} style={{ width: 90 }} onChange={onTimeIntervalChange}> {timeIntervalList.map((child) => ( <Option key={child.key} unit={child.unit} value={child.key}> {child.name} </Option> ))} </Select> </div> </> ); }; const getSeriesType = (sensorName) => { return sensorName ? (sensorName.indexOf('流量') > -1 ? 'area' : 'spline') : 'spline'; }; // 处理图表options const handleChartOptions = () => { const { series } = chartOptions; let _series = []; let _yAxis = []; let uniqueUnit = []; series.forEach((item, index) => { // 处理series let _s = { name: item.name, type: getSeriesType(item.sensorName), data: item.data, zIndex: 1, tooltip: { valueSuffix: item.unit ? item.unit : '' }, color: colors[index], decimalPoint: item.decimalPoint, navigatorOptions: { enabled: true, }, }; if (_s.type === 'area' || _s.type === 'areaspline') { _s.fillColor = { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1, }, stops: [ [0, Highcharts.Color(_s.color).setOpacity(0.1).get('rgba')], [1, '#fff'], ], }; _s.threshold = 0; } // 处理yAxis if (!uniqueUnit.includes(item.unit)) { uniqueUnit.push(item.unit); let _length = uniqueUnit.length - 1; let _y = { title: { text: item.unit, align: 'high', offset: 0, rotation: 0, y: -25, x: 0, }, gridLineWidth: 1, gridLineDashStyle: 'dash', lineWidth: 1, tickAmount: 10, crosshair: true, floor: 0, num: _length, opposite: _length % 2 === 0, offset: Math.floor(_length / 2) * 40, style: { color: '', }, labels: { style: { color: '', }, x: -2, }, }; _yAxis.push(_y); } // 处理series _s.yAxis = uniqueUnit.findIndex((child) => child === item.unit); _series.push(_s); }); Highcharts.setOptions({ global: { timezoneOffset: -8 * 60 }, }); let options = { ...defaultOptions }; if (CheckboxData[0].checked) { _yAxis = setYaxisMin(_yAxis, _series); } else { _yAxis = _yAxis.map((item) => ({ ...item, max: null, min: null })); } if (_yAxis.length > 0) { options = { ...defaultOptions, ...chartOptions, yAxis: _yAxis, series: _series, }; } options.tooltip.formatter = function formatter() { let _html = `<b>${Highcharts.dateFormat( contrastOption === 'day' ? '%H:%M' : '%d %H:%M', this.x, )}</b><br/>`; this.points.forEach((item) => { _html += `<span style={{color: ${item.series.color}}}>${item.series.name}</span>: <b>${ item.point.y.toFixed( item.series.userOptions.decimalPoint ? item.series.userOptions.decimalPoint : 2, ) * 1 }${item.series.userOptions.tooltip.valueSuffix}</b> <br/>`; }); return _html; }; if (container.current) { if (container.current.offsetWidth !== 0) { chartWidth = container.current.offsetWidth; chartHeight = container.current.offsetHeight; } Highcharts.setOptions({ // 处理 chart 高度坍塌 chart: { width: container.current.offsetWidth === 0 ? chartWidth : container.current.offsetWidth, height: container.current.offsetHeight === 0 ? chartHeight : container.current.offsetHeight, }, }); } return options; }; const setYaxisMin = (y, data) => { let result = y.concat(); data.forEach((val) => { let min = 999999999; let showMin = 999999999; let max = -999999999; let showMax = -999999999; val.data.forEach((item) => { if (item[1]) { min = Math.min(min, item[1]); showMin = Math.min(min, item[1]); max = Math.max(max, item[1]); showMax = Math.max(max, item[1]); } }); let k = 0; let same = false; for (let i = 0; i < val.data.length; i++) { // 判断是否全为0 if (val.data[i][1] !== 0) { k = 1; } // 判断是否全相等 if (i >= 1 && val.data[i][1] !== val.data[i - 1][1]) { same = true; } } if (k === 0) { result[val.yAxis].min = result[val.yAxis].min ? Math.min(result[val.yAxis].min, -0.2) : -0.2; result[val.yAxis].max = result[val.yAxis].max ? Math.max(result[val.yAxis].max, 0.2) : 0.2; } else if (!same) { min = val.data[0][1] > 0 ? val.data[0][1] * 0.5 : val.data[0][1] * 1.5; max = val.data[0][1] > 0 ? val.data[0][1] * 1.5 : val.data[0][1] * 0.5; result[val.yAxis].min = result[val.yAxis].min ? Math.min(result[val.yAxis].min, showMin) : min; result[val.yAxis].max = result[val.yAxis].max ? Math.max(result[val.yAxis].max, max) : max; } else { result[val.yAxis].min = result[val.yAxis].min ? Math.min(result[val.yAxis].min, min) : showMin; result[val.yAxis].max = result[val.yAxis].max ? Math.max(result[val.yAxis].max, max) : showMax; } }); return result; }; return ( <div className={classNames(prefixCls)}> <Tabs activeKey={activeTabKey} centered tabBarExtraContent={{ left: <h3 className="tabs-extra-demo-button">{title}</h3>, }} onChange={(key) => setActiveTabKey(key)} > {TabPaneData.map((item) => ( <TabPane tab={item.tab} key={item.key}> <div className={classNames(`${prefixCls}-content`)}> {renderOptions(item)} {!dataSource.length && <Empty />} {!!dataSource.length && ( <div className={classNames(`${prefixCls}-wrap`)}> <div className={classNames(`${prefixCls}-main`)}> {item.key === 'curve' && ( <div className={classNames(`${prefixCls}-chart`)} ref={container}> <HighchartsReact immutable={true} highcharts={Highcharts} constructorType={'stockChart'} options={options} allowChartUpdate={true} /> </div> )} {item.key === 'table' && ( <BasicTable dataSource={dataSource} columns={columns} {...tableProps} /> )} </div> </div> )} </div> </TabPane> ))} </Tabs> </div> ); }; HistoryInfo.defaultProps = { title: '指标曲线', defaultChecked: 'oneHour', columns: [], dataSource: [], tableProps: {}, chartOptions: {}, onChange: () => {}, }; HistoryInfo.propTypes = { title: PropTypes.string, defaultChecked: PropTypes.string, columns: PropTypes.array, dataSource: PropTypes.array, tableProps: PropTypes.object, chartOptions: PropTypes.object, onChange: PropTypes.func, }; export default HistoryInfo; const startFormat = 'YYYY-MM-DD 00:00:00'; const endFormat = 'YYYY-MM-DD 23:59:59'; const timeFormat = 'YYYY-MM-DD HH:mm:ss'; const colors = [ '#1884EC', '#90CE53', '#86E0C7', '#68cbd1', '#bb98d1', '#588c66', '#b0859e', '#647fac', '#7c6894', '#9c8273', '#838b61', '#437db0', '#9b97c4', '#bda589', '#89bd8e', '#cbcc75', ]; const defaultOptions = { chart: { zoomType: 'x', backgroundColor: 'rgba(255, 255, 255, 0.5)', }, colors: colors, title: null, credits: false, rangeSelector: { enabled: false, }, xAxis: [ { lineWidth: 0, crosshair: true, type: 'datetime', gridLineDashStyle: 'dash', gridLineWidth: 1, dateTimeLabelFormats: { second: '%H:%M:%S', minute: '%H:%M', hour: '%H:%M', day: '%d', week: '%d', month: '%d', year: '%Y', }, }, ], yAxis: [], tooltip: { shared: true, split: false, valueDecimals: 3, }, plotOptions: { series: { showInNavigator: true, connectNulls: false, zoneAxis: 'x', }, }, legend: { enabled: true, verticalAlign: 'top', }, series: [], responsive: { rules: [ { condition: { maxWidth: 800, minHeight: 500, }, }, ], }, }; const initialState = { dateRange: [], ignoreOutliers: false, isVertical: false, zoom: '', unit: '', }; const TabPaneData = [ { key: 'curve', tab: '曲线', }, { key: 'table', tab: '表格', }, ]; const CheckboxData = [ { key: 'curveCenter', label: '曲线居中', checked: false, }, { key: 'ignoreOutliers', label: '过滤异常值', type: 'updateIgnoreOutliers', checked: false, }, { key: 'dataThin', label: '数据抽稀', type: 'updateDataThin', checked: false, }, ]; const timeList = [ { key: 'oneHour', name: '近1小时', }, { key: 'fourHour', name: '近4小时', }, { key: 'twelveHours', name: '近12小时', }, { key: 'roundClock', name: '近24小时', }, { key: 'yesterday', name: '昨天', }, ]; const timeIntervalList = [ { key: '5', unit: 'min', name: '5分钟', }, { key: '10', unit: 'min', name: '10分钟', }, { key: '30', unit: 'min', name: '30分钟', }, { key: '1', unit: 'h', name: '1小时', }, { key: '2', unit: 'h', name: '2小时', }, { key: '6', unit: 'h', name: '6小时', }, { key: '12', unit: 'h', name: '12小时', }, ]; const DataPickerArr = [ { key: 1, value: moment(), }, { key: 2, value: moment().subtract(1, 'days'), }, ];