import React, {useContext, useEffect, useMemo, useState} from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Checkbox, ConfigProvider, DatePicker, Radio, Select, Spin, Tabs, Tooltip, Button, } from 'antd'; import { CloseCircleFilled, PlusCircleOutlined, QuestionCircleFilled, DownloadOutlined, } from '@ant-design/icons'; import moment from 'moment'; import _ from 'lodash'; import TimeRangePicker from '@wisdom-components/timerangepicker'; import PandaEmpty from '@wisdom-components/empty'; import BasicTable from '@wisdom-components/basictable'; import {getHistoryInfo, getDeviceAlarmScheme, getExportDeviceHistoryUrl} from './apis'; import SimgleChart from './SingleChart'; import GridChart from './GridChart'; import './index.less'; import {globalConfig} from 'antd/lib/config-provider'; const {RangePicker} = DatePicker; const {Option} = Select; 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 dateFormat = 'YYYYMMDD'; const timeList = [ { key: 'twelveHours', name: '近12小时', }, { key: 'roundClock', name: '近24小时', }, { key: 'oneWeek', name: '近1周', }, { key: 'oneMonth', name: '近1月', }, ]; const CheckboxData = [ { key: 'curveCenter', label: '曲线居中', checked: false, showInCurve: true, showInTable: false, }, { key: 'chartGrid', label: '图表网格', checked: true, showInCurve: true, showInTable: false, }, { key: 'ignoreOutliers', label: '数据滤波', type: 'updateIgnoreOutliers', checked: false, showInCurve: true, showInTable: true, tooltip: '数据滤波针对与样本平均值相差2个标准差以上的值进行过滤。', }, { key: 'justLine', label: '仅查看曲线', type: '', checked: false, showInCurve: false, showInTable: false, }, { key: 'dataThin', label: '数据抽稀', type: 'updateDataThin', checked: true, showInCurve: false, showInTable: true, }, ]; const timeIntervalList = [ { key: '5', zoom: '5', unit: 'min', name: '5分钟', }, { key: '10', zoom: '10', unit: 'min', name: '10分钟', }, { key: '30', zoom: '30', unit: 'min', name: '30分钟', }, { key: '1', zoom: '1', unit: 'h', name: '1小时', }, { key: '2', zoom: '2', unit: 'h', name: '2小时', }, { key: '4', zoom: '4', unit: 'h', name: '4小时', }, { key: '6', zoom: '6', unit: 'h', name: '6小时', }, { key: '12', zoom: '12', unit: 'h', name: '12小时', }, ]; const updateTime = (key) => { let start = ''; let end = ''; if (Array.isArray(key)) { start = moment(key[0]).format(timeFormat); end = moment(key[1]).format(timeFormat); } else { switch (key) { 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 'oneWeek': start = moment().subtract(7, 'day').format(timeFormat); end = moment().format(timeFormat); break; case 'oneMonth': start = moment().subtract(30, 'day').format(timeFormat); end = moment().format(timeFormat); break; } } return [ { dateFrom: start, dateTo: end, }, ]; }; const DefaultDatePicker = (value) => [ { key: 1, value: moment(), }, { key: 2, value: moment().subtract(1, value), }, ]; const handleBatchTime = (arr, cOption) => { let newArr = []; arr.forEach((child) => { if (child.value) { newArr.push({ dateFrom: moment(child.value).startOf(cOption).format(startFormat), dateTo: moment(child.value).endOf(cOption).format(endFormat), }); } }); newArr = _.uniqWith(newArr, _.isEqual); // 去掉重复日期时间 return newArr; }; const timeColumn = { title: '采集时间', dataIndex: 'time', key: 'time', width: 170, fixed: 'left', ellipsis: true, align: 'center', }; const HistoryView = (props) => { const {getPrefixCls} = useContext(ConfigProvider.ConfigContext); const prefixCls = getPrefixCls('history-view'); const { title, grid, defaultChecked, tableProps, deviceParams, defaultModel, showModels, needMarkLine, } = props; const isBoxPlots = deviceParams?.length === 1 && deviceParams[0]?.sensors?.split(',').length === 1; const [loading, setLoading] = useState(false); const [activeTabKey, setActiveTabKey] = useState(defaultModel); // 时间模式: 自定义模式/同期对比模式 const [timeValue, setTimeValue] = useState('customer'); // 自定义模式 const [customerChecked, setCustomerChecked] = useState(defaultChecked); // 时间快速选择类型值 const [customerTime, setCustomerTime] = useState(); // 自定义时间选择值 // 同期对比模式 const [contrastOption, setContrastOption] = useState('day'); // 对比时间类型: 日/月 const [datePickerArr, setDatePickerArr] = useState(DefaultDatePicker('day')); // 对比时间段配置值 const [checkboxData, setCheckboxData] = useState(() => [...CheckboxData]); // 曲线设置项 const [dataThinKey, setDataThinKey] = useState(timeIntervalList[0].key); // 曲线抽稀时间设置 const [columns, setColumns] = useState([]); const [tableData, setTableData] = useState([]); const [chartDataSource, setChartDataSource] = useState([]); const [chartType, setChartType] = useState('lineChart'); // 选择的时间范围值 const dateRange = useMemo(() => { if (timeValue === 'customer') { return updateTime(customerChecked || customerTime); } else { return handleBatchTime(datePickerArr, contrastOption); } }, [contrastOption, customerChecked, customerTime, datePickerArr, timeValue]); const configDependence = checkboxData .filter((item) => ['curveCenter', 'chartGrid'].indexOf(item.key) === -1) .map((item) => item.checked) .join(','); // 数据配置 const dataConfig = useMemo(() => { const initial = { ignoreOutliers: false, dataThin: false, zoom: '', // 数据抽稀时间 unit: '', // 数据抽稀时间单位 }; // 曲线居中,过滤异常值,数据抽稀 const config = checkboxData.reduce( (pre, item) => (item.key !== 'curveCenter' && (pre[item.key] = item.checked), pre), initial, ); // 数据抽稀时间单位 const dataThin = timeIntervalList.find((item) => item.key === dataThinKey); config.zoom = activeTabKey === 'curve' ? '' : dataThin?.zoom ?? ''; config.unit = activeTabKey === 'curve' ? '' : dataThin?.unit ?? ''; config.dataThin = activeTabKey === 'curve' ? true : config.dataThin; // 曲线强制抽稀 return config; }, [configDependence, dataThinKey, activeTabKey]); // 图表居中 const [curveCenter, chartGrid] = useMemo(() => { const curveCenter = checkboxData.find((item) => item.key === 'curveCenter')?.checked; const chartGrid = checkboxData.find((item) => item.key === 'chartGrid')?.checked; return [curveCenter, chartGrid]; }, [checkboxData]); // 自定义模式: 快速选择 const onCustomerTimeChange = (key) => { setCustomerChecked(key); !!customerTime && setCustomerTime(null); }; // 自定义模式: 自定义时间选择 const onCustomerRangeChange = (value) => { if (!value) { // 时间清空,回到默认时间选择 setCustomerChecked(defaultChecked); setCustomerTime(value); } else { setCustomerChecked(null); setCustomerTime(value); } }; // 同期对比模式: 选择(日/月) const onContrastChange = (value) => { setContrastOption(value); setDatePickerArr([...DefaultDatePicker(value)]); }; // 同期对比模式: 时间段选择 const onContrastPickerChange = (date, dateString, item) => { const arr = [...datePickerArr]; arr.forEach((child) => { if (child.key === item.key) { child.value = date; } }); setDatePickerArr(arr); }; // 同期对比模式: 新增日期选择组件 const handleAddDatePicker = () => { setDatePickerArr([ ...datePickerArr, { key: datePickerArr[datePickerArr.length - 1].key + 1, value: '', }, ]); }; // 同期对比模式: 删除日期选择组件 const handleDeleteDatePicker = (index) => { const arr = [...datePickerArr]; arr.splice(index, 1); setDatePickerArr(arr); }; // 时间设置切换(自定义/同期对比) const onTimeSetChange = (e) => { setTimeValue(e.target.value); if (e.target.value === 'contrast') { // 同期对比 onContrastChange(contrastOption); } else { // 自定义 // 不需要处理 } }; const renderTimeOption = () => { return ( <div className={classNames(`${prefixCls}-date`)}> <div className={classNames(`${prefixCls}-label`)}>时间选择</div> <Radio.Group value={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}-custime-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}-contrast-wrap`)}> <DatePicker picker={contrastOption} value={child.value} onChange={(date, dateString) => onContrastPickerChange(date, dateString, child)} /> {datePickerArr.length > 2 && ( <div className={classNames(`${prefixCls}-contrast-delete`)} onClick={() => handleDeleteDatePicker(index)} > <CloseCircleFilled/> </div> )} </div> {index < datePickerArr.length - 1 && ( <div className={classNames(`${prefixCls}-contrast-connect`)}>与</div> )} </div> ))} {datePickerArr.length < 5 && <PlusCircleOutlined onClick={handleAddDatePicker}/>} </> )} </div> ); }; // 曲线设置项选择/取消 const onCheckboxChange = (e, key) => { let data = [...checkboxData]; let _index = data.findIndex(item => item.key === 'justLine'); // 仅查看曲线会在勾选了数据滤波后展示 let _index1 = data.findIndex(item => item.key === 'ignoreOutliers'); // 仅查看曲线会在勾选了数据滤波后展示 data.forEach((item) => { if (item.key === key) { item.checked = e.target.checked; } }); if (key === 'ignoreOutliers') { data[_index].showInCurve = e.target.checked; data[_index].checked = e.target.checked; } if (key === 'chartType') { data[_index1].showInCurve = e.target.value; data[_index1].checked = false; } debugger setCheckboxData(data); }; // 数据抽稀时间间隔 const onTimeIntervalChange = (value) => { setDataThinKey(value); }; const renderCheckbox = (child) => { const curveAccess = activeTabKey === 'curve' && child.showInCurve; const tableAccess = activeTabKey === 'table' && child.showInTable; const gridOptions = ['curveCenter']; if (grid && curveAccess && gridOptions.indexOf(child.key) === -1) return null; return ( (curveAccess || tableAccess) && ( <> <Checkbox checked={child.checked} onChange={(e) => onCheckboxChange(e, child.key)}> {child.label} </Checkbox> {child.tooltip && ( <Tooltip title={child.tooltip}> <QuestionCircleFilled className={`${prefixCls}-question`}/> </Tooltip> )} </> ) ); }; const renderCurveOption = (isChart, isSingle) => { return ( <div className={classNames(`${prefixCls}-cover`)}> { isChart && isSingle ? <> <div className={classNames(`${prefixCls}-label`)}>曲线形态</div> <Radio.Group value={chartType} onChange={(e) => { let _value = e.target.value; setChartType(_value); onCheckboxChange({target: {value: _value !== 'boxChart'}}, 'chartType') }}> <Radio.Button value={'lineChart'}>线形图</Radio.Button> <Radio.Button value={'boxChart'}>箱线图</Radio.Button> </Radio.Group></> : '' } <div className={classNames(`${prefixCls}-label`)}>曲线设置</div> {checkboxData.map((child) => { const box = renderCheckbox(child); if (!box) return null; return ( <div key={child.key} className={`${prefixCls}-cover-item`}> {box} </div> ); })} {activeTabKey === 'table' && ( <Select value={dataThinKey} style={{width: 90}} onChange={onTimeIntervalChange} disabled={!dataConfig.dataThin} > {timeIntervalList.map((child) => ( <Option key={child.key} unit={child.unit} value={child.key}> {child.name} </Option> ))} </Select> )} </div> ); }; const exportExcelBtn = () => { deviceParams.forEach((i, r) => { let timeFrom = dateRange[r]?.dateFrom || moment().format(startFormat); let timeTo = dateRange[r]?.dateTo || moment().format(timeFormat); let fileName = `数据报表-${i.deviceType}-${i.deviceCode}-${moment(timeFrom).format( dateFormat, )}至${moment(timeTo).format(dateFormat)}`; getExportDeviceHistoryUrl({ deviceType: i.deviceType, deviceCode: i.deviceCode, quotas: i.sensors, startTime: timeFrom, endTime: timeTo, fileName: fileName, }) .then((res) => { if (res && res.code === -1) return message.error(res.msg); const url = `${window.location.origin}/PandaCore/GCK/FileHandleContoller/Download/name?name=${res.data}&_site=${globalConfig?.userInfo?.site}`; const aDom = document.createElement('a'); aDom.href = url; aDom.click(); aDom.remove(); }) .catch((err) => { }); }); }; const handleTableData = (data) => { const ignoreOutliers = checkboxData.find((item) => item.key === 'ignoreOutliers').checked; const dataIndexAccess = (dataItem, index) => { const {stationCode, sensorName} = dataItem; return `${stationCode}-${sensorName}-${index}`; }; let format = timeFormat; if (timeValue === 'contrast') { format = contrastOption === 'day' ? '2020-01-01 HH:mm:00' : '2020-01-DD HH:mm:00'; } // 处理表头数据 const columnsData = data.map((item, index) => { const {stationCode, equipmentName, sensorName, unit, dataModel} = item; const dataIndex = dataIndexAccess(item, index); let col = { title: `${equipmentName}-${sensorName}${unit ? `(${unit})` : ''}`, dataIndex: dataIndex, key: dataIndex, ellipsis: true, align: 'center', }; // 同期对比 if (timeValue === 'contrast' && dataModel[0]) { const time = item.dataModel[0].pt .slice(0, contrastOption === 'day' ? 10 : 7) .replace(/-/g, ''); col.title = `${equipmentName}-${sensorName}-${time}`; } return col; }); // 格式化时间对齐数据, 生成行数 const timeData = {}; const buildDefaultData = (time) => { const obj = {key: time, time: time}; data.forEach((item, index) => { const dataIndex = dataIndexAccess(item, index); obj[dataIndex] = ''; }); return obj; }; data.forEach((item, index) => { const {stationCode, sensorName, dataModel} = item; dataModel.forEach((data) => { const formatTime = moment(data.pt).format(format); let time = formatTime; if (timeValue === 'contrast') { time = time.slice(contrastOption === 'day' ? 11 : 8, 16); } timeData[formatTime] = timeData[formatTime] || buildDefaultData(time); }); }); // 处理表格数据 data.forEach((child, index) => { const {dataModel} = child; const dataIndex = dataIndexAccess(child, index); dataModel.forEach((value, j) => { const formatTime = moment(value.pt).format(format); const dataRow = timeData[formatTime]; if (dataRow) { dataRow[dataIndex] = value.pv === null || value.pv === undefined ? '' : value.pv; } }); }); const timeSort = (a, b) => { let aa = a, bb = b; if (timeValue === 'contrast') { aa = a.slice(contrastOption === 'day' ? 11 : 8, 16); bb = b.slice(contrastOption === 'day' ? 11 : 8, 16); } return aa.localeCompare(bb); }; const times = Object.keys(timeData).sort(timeSort); const tableData = times.map((time) => timeData[time]); setColumns([timeColumn, ...columnsData]); setTableData(tableData); }; const [deviceAlarmSchemes, setDeviceAlarmSchemes] = useState([]); const beforChangeParams = (value = {}) => { if (!needMarkLine) return Promise.resolve(); return getDeviceAlarmScheme({ data: deviceParams.map((item) => ({ deviceType: item.deviceType, deviceCode: item.deviceCode, pointAddressID: item.pointAddressID, sensorName: item.sensors, })), }) .then((res) => { if (res.code === 0) setDeviceAlarmSchemes(res.data || []); else setDeviceAlarmSchemes([]); return Promise.resolve(); }) .catch((err) => { setDeviceAlarmSchemes([]); return Promise.resolve(); }); }; const handleDataThinKey = (diffDays) => { // edit by zy 根据选择的时长控制抽稀频度 if (diffDays >= 7 && diffDays < 15) { return {unit: 'h', zoom: '2'}; } else if (diffDays >= 15 && diffDays < 30) { return {unit: 'h', zoom: '4'}; } else if (diffDays >= 30) { return {unit: 'h', zoom: '6'}; } else if (diffDays < 7 && diffDays >= 2) { return {unit: 'min', zoom: '40'}; } else if (diffDays < 2 && diffDays >= 1) { return {unit: 'min', zoom: '30'}; } else { return {unit: 'min', zoom: '10'}; } }; // 处理接口服务参数的变化 const onChangeParams = (value = {}) => { const {dateRange, isDilute, ignoreOutliers, zoom, unit} = value; const requestArr = []; const acrossTables = []; deviceParams.forEach((i) => { if (i.sensors && i.deviceCode && i.deviceCode) acrossTables.push(_.omit(i, ['pointAddressID'])); }); if (!acrossTables?.length) { handleTableData([]); setChartDataSource([]); return; } dateRange.forEach((item) => { let _showLine = checkboxData.find(item => item.key === 'justLine'); const param = { isDilute, zoom, unit, ignoreOutliers, isVertical: false, // 是否查询竖表 dateFrom: item.dateFrom, dateTo: item.dateTo, acrossTables, isBoxPlots: isBoxPlots }; let diffDays = moment(item.dateTo).diff(moment(item.dateFrom), 'days'); let zoomParam = activeTabKey === 'curve' ? handleDataThinKey(diffDays) : {}; requestArr.push(getHistoryInfo({...param, ...zoomParam})); }); setLoading(true); Promise.all(requestArr).then((results) => { if (results.length) { let data = []; results.forEach((res, index) => { const {dateFrom, dateTo} = dateRange?.[index] ?? {}; if (res.code === 0 && res.data.length) { res.data.forEach((d) => { d.dateFrom = dateFrom; d.dateTo = dateTo; }); deviceParams.forEach((p) => { // 返回数据按查询指标顺序排序 const sensors = p.sensors?.split(',') ?? []; const list = sensors.map((s) => { const dataItem = res.data.find( (d) => d.stationCode === p.deviceCode && d.sensorName === s, ); dataItem.dateFrom = dateFrom; dataItem.dateTo = dateTo; return dataItem; }); data = data.concat(list); }); } }); setLoading(false); handleTableData(data); setChartDataSource(data); } }); }; useEffect(() => { const {dataThin, ignoreOutliers, zoom, unit} = dataConfig; beforChangeParams().finally(() => { onChangeParams({ isDilute: dataThin, ignoreOutliers, zoom, unit, dateRange, isBoxPlots: isBoxPlots }); }); }, [dateRange, dataConfig, deviceParams, chartType]); const renderPanel = (model) => { if (model === 'curve') { return ( <> <div className={`${prefixCls}-options`}> {renderTimeOption()} {renderCurveOption(true, (deviceParams?.length === 1 && deviceParams[0]?.sensors?.split(',').length === 1))} </div> <div className={`${prefixCls}-content`}> {!chartDataSource.length ? ( <PandaEmpty/> ) : grid === true ? ( <GridChart curveCenter={curveCenter} prefixCls={prefixCls} dataSource={chartDataSource} contrast={timeValue === 'contrast'} contrastOption={contrastOption} deviceAlarmSchemes={deviceAlarmSchemes} /> ) : ( <SimgleChart curveCenter={curveCenter} showGridLine={chartGrid} prefixCls={prefixCls} dataSource={chartDataSource} justLine={!!checkboxData.find(item => item.key === 'justLine' && item.checked)} chartType={isBoxPlots ? chartType : null} contrast={timeValue === 'contrast'} contrastOption={contrastOption} deviceAlarmSchemes={deviceAlarmSchemes} /> )} </div> </> ); } if (model === 'table') { return ( <> <div className={`${prefixCls}-options`}> {renderTimeOption()} {renderCurveOption()} </div> <div className={`${prefixCls}-content`}> {chartDataSource.length > 0 ? ( <BasicTable dataSource={tableData} columns={columns} {...tableProps} pagination={false} onChange={() => { }} /> ) : ( <PandaEmpty/> )} </div> </> ); } }; return ( <div className={classNames(prefixCls)}> <Spin spinning={loading} wrapperClassName={classNames(`${prefixCls}-spin`)}> {showModels.length === 1 && ( <div className={`${prefixCls}-single-panel`}>{renderPanel(showModels[0])}</div> )} {showModels.length > 1 && ( <Tabs activeKey={activeTabKey} onChange={(key) => setActiveTabKey(key)} centered tabBarExtraContent={{ left: <h3>{title}</h3>, right: ( <div className={`${prefixCls}-extra-right`}> {activeTabKey === 'table' && ( <Button type="link" onClick={exportExcelBtn}> <DownloadOutlined/> 下载 </Button> )} </div> ), }} > <Tabs.TabPane key="curve" tab="曲线"> {renderPanel('curve')} </Tabs.TabPane> <Tabs.TabPane key="table" tab="表格"> {renderPanel('table')} </Tabs.TabPane> </Tabs> )} </Spin> </div> ); }; HistoryView.propTypes = { grid: PropTypes.bool, title: PropTypes.string, defaultChecked: PropTypes.oneOf(['twelveHours', 'roundClock', 'oneWeek', 'oneMonth']), tableProps: PropTypes.object, deviceParams: PropTypes.arrayOf( PropTypes.objectOf({ deviceCode: PropTypes.string, sensors: PropTypes.string, deviceType: PropTypes.string, pointAddressID: PropTypes.number, // 可选,配置了将会查询相关报警方案配置 }), ), defaultModel: PropTypes.oneOf(['curve', 'table']), showModels: PropTypes.arrayOf(PropTypes.oneOf(['curve', 'table'])), }; HistoryView.defaultProps = { grid: false, title: '指标曲线', defaultChecked: 'roundClock', tableProps: {}, defaultModel: 'curve', showModels: ['curve', 'table'], needMarkLine: true, }; export default HistoryView;