/** * 走influxdb版本的,不抽稀通过zoom和unit传空字符串实现; * 非influxdb版本的接口,使用isDilute=false实现; * 建议:不抽稀的时候,传isDilute=false&zoom=''&unit='' * */ import React, {useContext, useEffect, useMemo, useState, useCallback, useRef} from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Checkbox, ConfigProvider, DatePicker, Radio, Select, Spin, Tabs, Tooltip, Button, message, Progress, } 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 { getHistoryInfo, getDeviceAlarmScheme, getExportDeviceHistoryUrl, getDictionaryInfoAll, getPointAddress, getPointAddressEntry, getPredicateSensor, } from './apis'; import SingleChart from './SingleChart'; import GridChart from './GridChart'; import BIStyles from './indexForBI.less'; import {globalConfig} from 'antd/lib/config-provider'; import {getSensorType} from './apis/index'; import {ExportExcel} from '@wisdom-components/exportexcel'; import VirtualTable from './VirtualTable'; 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: 'yesterday', name: '昨日', }, { key: 'oneWeek', name: '近1周', }, { key: 'oneMonth', name: '近1月', }, ]; const predicateMap = { twelveHours: 12, roundClock: 24, oneWeek: 24, oneMonth: 24 } // 同期对比 日 快捷按钮 const shortcutsForDay = [ { label: '近3天', value: '近3天', }, { label: '近7天', value: '近7天', }, /* { label: '去年同期', value: '去年同期', }*/ ]; // 同期对比 月 快捷按钮 const shortcutsForMonth = [ { label: '近3月', value: '近3月', }, { label: '近6月', value: '近6月', }, /* { label: '去年同期', value: '去年同期', }*/ ]; 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: '采用递推平均滤波法(滑动平均滤波法)对采样数据中的异常离群值进行识别与去除。', hasSub: true, }, { key: 'dataThin', label: '数据抽稀', type: 'updateDataThin', checked: true, showInCurve: false, showInTable: true, } ]; const timeIntervalList = [ { key: '1min', zoom: '1', unit: 'min', name: '1分钟', }, { 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小时', }, { key: '24', zoom: '24', unit: 'h', name: '24小时', }, ]; const handleTimeForPredicate = (key, start, end) => { let t = predicateMap[key] || 0; return moment(end).add(t, 'hours').format(timeFormat); }; const updateTime = (key, predicate) => { 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 'yesterday': start = moment().subtract(1, 'days').format('YYYY-MM-DD 00:00:00'); end = moment().subtract(1, 'days').format('YYYY-MM-DD 23:59:59'); 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; } } if (predicate) { end = handleTimeForPredicate(key, end) } 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 handleFakeData = (dateRange, deviceParams) => { let dateFrom = dateRange[0].dateFrom; let dateTo = dateRange[0].dateTo; return deviceParams.reduce((final, cur) => { let _arr = cur.sensors.split(','); _arr.forEach(sensor => { final.push({ dataModel: [ {pt: dateFrom, pv: null}, {pt: dateTo, pv: null} ], dateFrom, dateTo, deviceCode: cur.deviceCode, deviceType: cur.deviceType, sensorName: sensor, equipmentName: '', stationCode: cur.deviceCode }) }) return final; }, []) } const timeColumn = { title: '采集时间', dataIndex: 'time', key: 'time', width: 170, ellipsis: true, align: 'center', sorter: true, // sortOrder:['descend','ascend'] }; const OriginMaxDays = 31; // 原始曲线请求数据的最大天数 const CharacteristicMaxDays = null; // 特征曲线或者其他曲线的最大天数 const HistoryView = (props) => { const [completeInit, setCompleteInit] = useState(false); const {getPrefixCls} = useContext(ConfigProvider.ConfigContext); const prefixCls = getPrefixCls('history-view'); const { title, grid, defaultChecked, tableProps, deviceParams, defaultModel, showModels, needMarkLine, defaultDate, theme = "Normal" } = props; if (theme === 'Normal') import('./index.less'); if (theme === 'BI') import('./indexForBI.less'); const isBoxPlots = deviceParams?.length === 1 && deviceParams?.[0]?.sensors?.split(',').length === 1; const [loading, setLoading] = useState(null); 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(defaultDate)); // 对比时间段配置值 const [checkboxData, setCheckboxData] = useState(() => [...CheckboxData]); // 曲线设置项 const [dataThinKey, setDataThinKey] = useState( timeIntervalList[0].key === '1min' ? timeIntervalList[1].key : timeIntervalList[0].key, ); // 曲线抽稀时间设置 const [algorithmValue, setAlgorithmValue] = useState(1); const [columns, setColumns] = useState([]); const [tableData, setTableData] = useState([]); const [timeOrder, setTimeOrder] = useState('descend'); const [chartType, setChartType] = useState('lineChart'); const [showBoxOption, setShowBoxOption] = useState(true); const [lineDataType, setLineDataType] = useState('特征曲线'); // 同期对比快捷键 // shortcutsValue // onShortcutsChange const [shortcutsValue, setShortcutsValue] = useState(''); const [shortcutsDatePickerArr, setShortcutsDatePickerArr] = useState([]); const [percent, setPercent] = useState(0); // 频率指标特殊业务 const [special1, setSpecial1] = useState(null); const [allPointAddress, setAllPointAddress] = useState([]); //查询所有sensorType const [allSensorType, setAllSensorType] = useState([]); const [isSingleStatusSensor, setIsSingleStatusSensor] = useState(false); const [predicateDevice, setPredicateDevice] = useState(null); const [predicateData, setPredicateData] = useState([]); const [predicateTime, setPredicateTime] = useState(null); // 需要处理默认数据,确保图表能够一直显示坐标轴。用来存储当前的请求状态。 const emptyOrError = useRef({ empty: true, error: true }) // 这部分功能有问题,等待解决后上线 2024年3月13日 const [discreteDeviceType, setDiscreteDeviceType] = useState(['水厂']) // 历史数据相关的特征描述 const deviceConfig = useRef({ oneDevice: deviceParams.length === 1, //单设备 oneSensor: [ ...new Set( deviceParams.reduce((final, cur) => { let _sensors = cur.sensors.split(','); return final.concat(_sensors); }, []), ), ].length === 1, // 单指标 }); // 表格虚拟列表 const tableRef = useRef(); // 选择的时间范围值 const dateRange = useMemo(() => { if (timeValue === 'customer') { return updateTime(customerChecked || customerTime, predicateDevice); } else { let _dateArr = shortcutsValue ? shortcutsDatePickerArr : datePickerArr; return handleBatchTime(_dateArr, contrastOption); } }, [contrastOption, customerChecked, customerTime, datePickerArr, timeValue, shortcutsValue]); useEffect(() => { let _diffDays = moment(dateRange[0].dateTo).diff(dateRange[0].dateFrom, 'days'); if (_diffDays > 7 && dataThinKey === '1min') { setDataThinKey(timeIntervalList[1].key); } }, [dateRange]); const [dates, setDates] = useState(null); const [chartDataSource, setChartDataSource] = useState(handleFakeData(dateRange, deviceParams) ?? []); 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); setPredicateTime(predicateMap[key]); !!customerTime && setCustomerTime(null); }; // 自定义模式: 自定义时间选择 const onCustomerRangeChange = (value) => { if (!value) { // 时间清空,回到默认时间选择 setCustomerChecked(defaultChecked); setCustomerTime(value); } else { setCustomerChecked(null); let diffDays = moment(value[1]).diff(moment(value[0]), 'days'); if (diffDays > OriginMaxDays && lineDataType === '原始曲线') { setLineDataType('特征曲线'); message.info('时间区间超过7天,已切换为特征曲线'); } setCustomerTime(value); } }; // 同期对比模式: 选择(日/月) const onContrastChange = (value) => { if (value === 'month') { if (lineDataType === '原始曲线') message.info('月模式数据量较大,不支持原始曲线模式,已切换为特征曲线'); setLineDataType('特征曲线'); } setShortcutsValue(''); setContrastOption(value); // 模式为日时,默认对比时间根据defaultDate判断 是昨天、上月、还是去年 setDatePickerArr([...DefaultDatePicker(value === 'day' && defaultDate ? defaultDate : value)]); }; // 同期对比模式: 时间段选择 const onContrastPickerChange = (date, dateString, item) => { // 操作时间就清除掉快捷键选用状态 setShortcutsValue(''); const arr = [...datePickerArr]; arr.forEach((child) => { if (child.key === item.key) { child.value = date; } }); setDatePickerArr(arr); }; // 同期对比模式: 新增日期选择组件 const handleAddDatePicker = () => { // 操作时间就清除掉快捷键选用状态 setShortcutsValue(''); setDatePickerArr([ ...datePickerArr, { key: datePickerArr[datePickerArr.length - 1].key + 1, value: '', }, ]); }; // 同期对比模式: 删除日期选择组件 const handleDeleteDatePicker = (index) => { // 操作时间就清除掉快捷键选用状态 setShortcutsValue(''); const arr = [...datePickerArr]; arr.splice(index, 1); setDatePickerArr(arr); }; // 时间设置切换(自定义/同期对比) const onTimeSetChange = (e) => { // 操作时间就清除掉快捷键选用状态 setShortcutsValue(''); if (e.target.value === 'customer') { setLineDataType('特征曲线'); setShortcutsValue(''); } setTimeValue(e.target.value); if (e.target.value === 'contrast') { // 同期对比 onContrastChange(contrastOption); setShowBoxOption(false); setChartType('lineChart'); onCheckboxChange({target: {value: false}}, 'chartType'); onCheckboxChange({target: {value: false}}, 'ignoreOutliers'); } else { // 自定义 // 不需要处理 setShowBoxOption(true); onCheckboxChange({target: {value: true}}, 'chartType'); } }; const onShortcutsChange = (e) => { let _val = e.target.value; setShortcutsValue(_val); let _arr = []; switch (_val) { case '近3天': _arr = [ {key: 1, value: moment()}, {key: 2, value: moment().subtract(1, 'days')}, {key: 3, value: moment().subtract(2, 'days')}, ]; break; case '近7天': _arr = [ {key: 1, value: moment()}, {key: 2, value: moment().subtract(1, 'days')}, {key: 3, value: moment().subtract(2, 'days')}, {key: 4, value: moment().subtract(3, 'days')}, {key: 5, value: moment().subtract(4, 'days')}, {key: 6, value: moment().subtract(5, 'days')}, {key: 7, value: moment().subtract(6, 'days')}, ]; break; case '近3月': _arr = [ {key: 1, value: moment()}, {key: 2, value: moment().subtract(1, 'months')}, {key: 3, value: moment().subtract(2, 'months')}, ]; break; case '近6月': _arr = [ {key: 1, value: moment()}, {key: 2, value: moment().subtract(1, 'months')}, {key: 3, value: moment().subtract(2, 'months')}, {key: 4, value: moment().subtract(3, 'months')}, {key: 5, value: moment().subtract(4, 'months')}, {key: 6, value: moment().subtract(5, 'months')}, ]; break; } setShortcutsDatePickerArr(_arr); }; const renderTimeOption = useMemo(() => { return ( <div className={classNames(`${prefixCls}-date`)}> <div className={classNames(`${prefixCls}-label`)}>时间选择</div> <Radio.Group value={timeValue} onChange={onTimeSetChange}> <Radio.Button value="customer">自定义</Radio.Button> {!grid ? <Radio.Button value="contrast">同期对比</Radio.Button> : ''} </Radio.Group> {timeValue === 'customer' && ( // 自定义 <> <TimeRangePicker format={'YYYY-MM-DD HH:mm'} onChange={onCustomerTimeChange} value={customerChecked} dataSource={timeList} /> <RangePicker getPopupContainer={trigger => trigger.parentElement} format={'YYYY-MM-DD HH:mm'} className={classNames(`${prefixCls}-custime-customer`)} onChange={onCustomerRangeChange} value={dates || customerTime} onCalendarChange={(val) => { setDates(val); }} onOpenChange={(open) => { if (open) { setDates([null, null]); } else { setDates(null); } }} disabledDate={(current) => { if (timeValue !== 'customer') return false; let _days = lineDataType === '原始曲线' ? OriginMaxDays : CharacteristicMaxDays; if (!dates) { return false; } if (!_days) return false; const tooLate = dates[0] && current.diff(dates[0], 'days') > _days; const tooEarly = dates[1] && dates[1].diff(current, 'days') > _days; return !!tooEarly || !!tooLate; }} showTime={{ format: 'YYYY-MM-DD HH:mm', minuteStep: 10, }} /> </> )} {timeValue === 'contrast' && ( // 同期对比 <> <Select value={contrastOption} style={{width: 60}} onChange={onContrastChange}> <Option value="day">日</Option> <Option value="month" disabled={lineDataType === '原始曲线'}> 月 </Option> </Select> {/*增加快捷日期*/} {deviceParams?.length === 1 && deviceParams?.[0]?.sensors?.split(',').length === 1 ? ( <Radio.Group value={shortcutsValue} onChange={onShortcutsChange}> {(contrastOption === 'day' ? shortcutsForDay : shortcutsForMonth).map((item) => { return <Radio.Button value={item.value}>{item.label}</Radio.Button>; })} </Radio.Group> ) : ( '' )} {datePickerArr.map((child, index) => ( <div key={child.key} className={classNames(`${prefixCls}-contrast-list`)}> <div className={classNames(`${prefixCls}-contrast-wrap`)}> <DatePicker key={child.key} picker={contrastOption === 'day' ? undefined : contrastOption} value={child.value} onChange={(date, dateString) => onContrastPickerChange(date, dateString, child)} style={{width: 130, border: !shortcutsValue ? '1px solid #1890ff' : ''}} /> {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 < 4 && <PlusCircleOutlined onClick={handleAddDatePicker}/>} </> )} </div> ); }, [ timeValue, customerChecked, lineDataType, datePickerArr, deviceParams, dates, customerTime, chartDataSource, ]); // 曲线设置项选择/取消 const onCheckboxChange = (e, key, showJustLine) => { let data = [...checkboxData]; let _index1 = data.findIndex((item) => item.key === 'ignoreOutliers'); // 仅查看曲线会在勾选了数据滤波后展示 data.forEach((item) => { if (item.key === key) { item.checked = e.target.checked; } }); if (key === 'ignoreOutliers') { data[_index1].showInCurve = true; } if (key === 'chartType') { data[_index1].showInCurve = e.target.value; data[_index1].checked = false; // data[_index].showInCurve = false; // data[_index].checked = false; } setCheckboxData(data); }; // 数据抽稀时间间隔 const onTimeIntervalChange = (value) => { setDataThinKey(value); }; // 切换数据类型 const switchLineDataType = (e) => { let _val = e.target.value; let _startDate = dateRange[0]?.dateFrom; let _endDate = dateRange[0]?.dateTo; let diffDays = moment(_endDate).diff(moment(_startDate), 'days'); if (_val === '原始曲线' && diffDays > OriginMaxDays) { message.info('查阅原始曲线时,需选择小于或等于31天的时间间隔,已自动切换为近一月'); setCustomerChecked('oneMonth'); } if (_val === '原始曲线') { setContrastOption('day'); } setLineDataType(_val); }; const renderCheckbox = (child, showJustLine) => { 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> )} {child.hasSub && child.checked && false ? ( <Select style={{width: 80, marginLeft: 10}} value={algorithmValue} onChange={(e) => setAlgorithmValue(e)} > <Option value={1}>低</Option> <Option value={5}>中</Option> <Option value={10}>高</Option> </Select> ) : ( '' )} </> ) ); }; const renderCurveOption = (isChart, isSingle, isStatus) => { return ( <div className={classNames(`${prefixCls}-cover`)} style={isChart && isSingle ? {width: '100%'} : {}} > {isChart && !isStatus ? ( <> <div className={classNames(`${prefixCls}-label`)}>曲线选择</div> <div className={`${prefixCls}-cover-item`}> <Radio.Group value={lineDataType} onChange={switchLineDataType}> <Radio.Button value={'特征曲线'}>特征曲线</Radio.Button> <Radio.Button value={'原始曲线'}>原始曲线</Radio.Button> </Radio.Group> <Tooltip title={'原始曲线数据量较大,单次查询最多展示1万条数据'}> <QuestionCircleFilled style={{marginLeft: 6}} className={`${prefixCls}-question`} /> </Tooltip> </div> </> ) : ( '' )} {isChart && isSingle && showBoxOption && !isStatus && !grid ? ( <> {lineDataType !== '原始曲线' ? ( <> <div style={{marginLeft: 7}} className={classNames(`${prefixCls}-label`)}> 曲线形态 </div> <Radio.Group value={chartType} style={{marginRight: 16}} 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> </> ) : ( '' )} </> ) : ( '' )} {!isStatus ? ( <> <div className={classNames(`${prefixCls}-label`)}> {activeTabKey !== 'table' ? '曲线设置' : '表格设置'} </div> {checkboxData.map((child) => { const box = renderCheckbox(child, isChart && isSingle); 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} getPopupContainer={trigger => trigger.parentElement} > {timeIntervalList .filter((item) => { let _diffDays = moment(dateRange[0].dateTo).diff(dateRange[0].dateFrom, 'days'); return !(_diffDays > 7 && item.key === '1min'); }) .map((child) => { return ( <Option key={child.key} unit={child.unit} value={child.key}> {child.name} </Option> ); })} </Select> )} </> ) : ( '' )} </div> ); }; const exportExcelBtn = () => { message.info('报表生成中,请稍后~'); 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)}`; let _quotas = i.sensors .split(',') .filter((item) => item !== '是否在线') .join(','); getExportDeviceHistoryUrl({ deviceType: i.deviceType, deviceCode: i.deviceCode, quotas: _quotas, 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 exportFeatureBtn = () => { message.info('报表生成中,请稍后~'); let _dataSource = tableData.sort((a, b) => { let _a = a.time; let _b = b.time; if (timeValue === 'contrast') { if (contrastOption === 'day') { _a = `2000-01-01 ${a.time}:00`; _b = `2000-01-01 ${b.time}:00`; } if (contrastOption === 'month') { _a = `2000-01-${a.time}:00`; _b = `2000-01-${b.time}:00`; } } return timeOrder === 'ascend' ? moment(_a) - moment(_b) : moment(_b) - moment(_a); }); let _columns = [...columns]; let timeFrom = dateRange?.[0]?.dateFrom || moment().format(startFormat); let timeTo = dateRange?.[0]?.dateTo || moment().format(timeFormat); let fileName = `特征数据-${moment(timeFrom).format(dateFormat)}至${moment(timeTo).format( dateFormat, )}`; let _dataIndex = []; let _titleWidth = []; let _title = _columns.map((item) => { _dataIndex.push(item.dataIndex); let _titleArr = [item.name, item.title]; if (item.title.includes(item.name)) { _titleArr = [item.title]; } let _titleStr = _titleArr.filter((item) => item).join('-'); _titleWidth.push(_titleStr.length * 1); return _titleStr; }); ExportExcel({ name: fileName, content: [ { sheetData: _dataSource, sheetFilter: _dataIndex, sheetHeader: _title, columnWidths: _titleWidth, }, ], }); }; const handleTableData = useCallback( (data) => { // eslint-disable-next-line no-param-reassign // data = data.filter(item => item.sensorName !== '是否在线'); 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 _title = ''; if (deviceConfig.current.oneDevice) { _title = `${sensorName}${unit ? `(${unit})` : ''}`; } else { _title = `${equipmentName}-${sensorName}${unit ? `(${unit})` : ''}`; } let col = { title: _title, dataIndex: dataIndex, key: dataIndex, ellipsis: true, align: 'center', width: 200, name: equipmentName, }; // 同期对比 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] = ''; obj.name = item.equipmentName; }); return obj; }; data.forEach((item, index) => { const {stationCode, sensorName, dataModel} = item; dataModel && 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 && 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 timeOrder === 'descend' ? -aa.localeCompare(bb) : aa.localeCompare(bb); }; const times = Object.keys(timeData).sort(timeSort); const tableData = times.map((time) => timeData[time]); setColumns([timeColumn, ...columnsData]); setTableData(tableData); }, [timeOrder, timeValue, contrastOption], ); const [deviceAlarmSchemes, setDeviceAlarmSchemes] = useState([]); const beforChangeParams = (value = {}) => { // 不需要报警标线 或者 多曲线、多设备时不显示报警标线 let _lengthEqual0 = deviceParams?.length === 0; let _lengthEqual1 = deviceParams?.length === 1; let _lengthMoreThan1 = deviceParams?.length > 1; let returnNothing = !needMarkLine || _lengthEqual0 || _lengthMoreThan1 || (_lengthEqual1 && deviceParams[0].sensors.split(',').length > 1) if (returnNothing) 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 = (diffYears, diffDays, diffHours, lineDataType) => { if (lineDataType === '原始曲线') { return {unit: '', zoom: ''}; } // edit by zy 根据选择的时长控制抽稀频度 if (diffYears > 0) { if (diffYears === 1) return {unit: 'h', zoom: '24'}; return {unit: 'h', zoom: '48'}; } else if (diffYears === 0 && diffDays > 0) { if (diffDays > 90) return {unit: 'h', zoom: '24'}; if (diffDays > 30) return {unit: 'h', zoom: '4'}; if (diffDays > 15) return {unit: 'h', zoom: '2'}; if (diffDays > 7) return {unit: 'h', zoom: '1'}; if (diffDays > 3) return {unit: 'min', zoom: '20'}; if (diffDays > 1) return {unit: 'min', zoom: '15'}; if (diffDays === 1) return {unit: 'min', zoom: '5'}; } else if (diffYears === 0 && diffDays === 0 && diffHours > 0) { if (diffHours > 12) return {unit: 'min', zoom: '5'}; if (diffHours > 4) return {unit: 'min', zoom: '1'}; if (diffHours > 1) return {unit: 's', zoom: '30'}; if (diffHours > 0) return {unit: 's', zoom: '5'}; return {unit: 's', zoom: '5'}; } else { return {unit: '', zoom: ''}; } }; // 处理接口服务参数的变化 const onChangeParams = (value = {}) => { const {dateRange, isDilute, ignoreOutliers, zoom, unit} = value; let _diffDays = moment(dateRange[0].dateTo).diff(dateRange[0].dateFrom, 'days'); // 查询时段大于7天时,不提供1分钟的抽稀选项。 if (_diffDays > 7 && zoom === '1' && unit === 'min') { return false; } const requestArr = []; const acrossTables = []; const zoomArray = []; // 这部分功能有问题,等待解决后上线 2024年3月13日 let hasDiscreteDeviceType = false; deviceParams .map((item) => { let _item = {...item}; _item.sensors = item.sensors; // special 业务 if (special1) { _item.sensors += `,${special1.name}`; } return _item; }) .forEach((i) => { if (i.sensors && i.deviceCode) acrossTables.push(_.omit(i, ['pointAddressID'])); // 这部分功能有问题,等待解决后上线 2024年3月13日 if (discreteDeviceType.includes(i.deviceType)) { hasDiscreteDeviceType = true; } }); if (!acrossTables?.length || !dateRange.length) { handleTableData([]); setChartDataSource([]); return; } dateRange.forEach((item) => { const param = { isDilute, zoom, unit, ignoreOutliers, dateFrom: item.dateFrom, dateTo: item.dateTo, acrossTables, isBoxPlots: isBoxPlots, }; let diffYears = moment(item.dateTo).diff(moment(item.dateFrom), 'years'); let diffDays = moment(item.dateTo).diff(moment(item.dateFrom), 'days'); let diffHours = moment(item.dateTo).diff(moment(item.dateFrom), 'hours'); let zoomParam = activeTabKey === 'curve' ? handleDataThinKey(diffYears, diffDays, diffHours, lineDataType) : !isDilute ? {zoom: '', unit: ''} : {}; // 表格也支持全数据模式; let _finalParams = {...param, ...zoomParam}; // 2024年1月8日 抽稀间隔大于等于12小时时,会存在线性插值导致抽稀间隔内数据条数大于预期的问题。需要增加一个额外参数处理该情况。 if (_finalParams.zoom) { let _num = Number(_finalParams.zoom); let _isNaN = isNaN(_num); if (!_isNaN && _num >= 12) _finalParams.isInterpolation = false; } // 2024年1月23日 增加预测曲线,单设备单曲线 // 同期对比不允许、多设备的不允许预测 if (dateRange.length === 1 && predicateDevice) { _finalParams.acrossTables.push(predicateDevice); } // 2024年3月11日 如果设备是某些特殊设备,则采用离散型算法 // 这部分功能有问题,等待解决后上线 2024年3月13日 if (hasDiscreteDeviceType && ignoreOutliers) { _finalParams.algorithmName = "derivative"; } requestArr.push(getHistoryInfo(_finalParams)); }); setLoading(true); Promise.all(requestArr) .then((results) => { setLoading(false); emptyOrError.current.error = false; if (results.length) { let data = []; // let _predicateData = []; 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 || ''; /** * @date: 2023年10月25日 * @description: 数据连续补点之后,首值、尾值、最大值、最小值不会补,都为null。 * 为保证显示,将补点之后的数据的首值、尾值、最大值、最小值同时为null的情况变更为点的值。 * * 请注意,此项为重要变更,此变更会影响原始数据。 */ // d.dataModel=[]; d.dataModel = d.dataModel.map((item) => { let {firstPV, lastPV, maxPV, minPV, pv} = item; if (pv !== null && firstPV === null && lastPV === null && maxPV === null && minPV === null) { firstPV = pv; lastPV = pv; maxPV = pv; minPV = pv; } return { ...item, firstPV, lastPV, maxPV, minPV, }; }); }); // 加入预测 (predicateDevice ? deviceParams.concat(predicateDevice) : deviceParams).forEach((p) => { // 返回数据按查询指标顺序排序 const sensors = p.sensors?.split(',') ?? []; if (sensors?.length) { sensors.push('是否在线'); if (special1) { sensors.push(special1.name); } } const list = sensors.map((s) => { const dataItem = res.data.find( (d) => d.stationCode === p.deviceCode && d.sensorName === s, ); if (dataItem) { dataItem.dateFrom = dateFrom || ''; dataItem.dateTo = dateTo || ''; dataItem.deviceType = p.deviceType; return dataItem; } else { return {}; } }).filter((item) => item.sensorName); // 预测的 data = data.concat(list); // _predicateData = _predicateData.concat(list.filter(item => item.deviceType === '预测')); }); } }); setLoading(false); if (data.length !== 0) { emptyOrError.current.empty = false; } else { data = handleFakeData(dateRange, deviceParams) ?? [] } handleTableData(data) setChartDataSource(data); // setPredicateData(_predicateData); } }) .catch((err) => { let data = handleFakeData(dateRange, deviceParams) ?? []; handleTableData(data); setChartDataSource(data); message.info('未查询到数据,请重试~'); setLoading(false); }); }; useEffect(() => { if (!completeInit) return; const {dataThin, ignoreOutliers, zoom, unit} = dataConfig; beforChangeParams().finally(() => { onChangeParams({ isDilute: dataThin, ignoreOutliers, zoom, unit, dateRange, isBoxPlots: isBoxPlots, }); }); }, [dateRange, dataConfig, deviceParams, chartType, lineDataType, completeInit, algorithmValue]); const handleChange = (pagination, filter, sort) => { if (sort.field === 'time') { setTimeOrder(sort.order); } }; const tableMemo = useMemo(() => { return ( <> <div className={`${prefixCls}-options`}> {renderTimeOption} {renderCurveOption()} </div> <div className={`${prefixCls}-content`} ref={tableRef}> {chartDataSource.length > 0 ? ( // <BasicTable <VirtualTable className={`${prefixCls}-virtual-table`} theme={theme} dataSource={tableData.sort((a, b) => { let _a = a.time; let _b = b.time; if (timeValue === 'contrast') { if (contrastOption === 'day') { _a = `2000-01-01 ${a.time}:00`; _b = `2000-01-01 ${b.time}:00`; } if (contrastOption === 'month') { _a = `2000-01-${a.time}:00`; _b = `2000-01-${b.time}:00`; } } return timeOrder === 'ascend' ? moment(_a) - moment(_b) : moment(_b) - moment(_a); })} columns={columns} tableProps={tableProps} pagination={false} onChange={handleChange} // scroll={{ x: 'max-content', y: 'calc(100% - 40px)' }} scroll={{ x: 'max-content', y: tableRef.current ? tableRef.current.getBoundingClientRect().height - 40 : 0, }} /> ) : ( <PandaEmpty/> )} </div> </> ); }, [ timeOrder, chartDataSource, columns, tableProps, tableData, isSingleStatusSensor, dateRange, tableRef.current, ]); const returnLongestPeriod = (data) => { let _earliest = ''; let _latest = ''; data.forEach((item) => { let _length = item.dataModel.length; let _tempFirst = item.dataModel[0].pt; let _tempLast = item.dataModel[_length - 1].pt; if (_earliest) { _earliest = moment(_earliest) > moment(_tempFirst) ? _tempFirst : _earliest; } else { _earliest = _tempFirst; } if (_latest) { _latest = moment(_latest) < moment(_tempLast) ? _tempLast : _latest; } else { _latest = _tempLast; } }); return `${_earliest} - ${_latest}`; }; const renderPanel = (model) => { if (model === 'curve') { return ( <> <div className={`${prefixCls}-options`}> {renderTimeOption} {renderCurveOption( true, deviceParams?.length === 1 && deviceParams?.[0]?.sensors?.split(',').length === 1, isSingleStatusSensor, )} </div> {lineDataType === '原始曲线' && false ? ( <div style={{marginTop: 10}}>展示区间:{returnLongestPeriod(chartDataSource)}</div> ) : ( '' )} <div className={`${prefixCls}-content`}> {grid === true ? ( <GridChart emptyOrError={emptyOrError.current} curveCenter={curveCenter} prefixCls={prefixCls} dataSource={chartDataSource} contrast={timeValue === 'contrast'} contrastOption={contrastOption} deviceAlarmSchemes={deviceAlarmSchemes} dateRange={dateRange} allPointAddress={allPointAddress} allSensorType={allSensorType} loading={loading} setLoading={setLoading} /> ) : ( <SingleChart emptyOrError={emptyOrError.current} dateRange={dateRange} showBoxOption={showBoxOption} lineDataType={lineDataType} curveCenter={curveCenter} showGridLine={chartGrid} prefixCls={prefixCls} dataSource={chartDataSource} predicateData={predicateData} chartType={isBoxPlots ? chartType : null} contrast={timeValue === 'contrast'} contrastOption={contrastOption} deviceAlarmSchemes={deviceAlarmSchemes} theme={theme} special={{ special1, // 频率业务 allPointAddress, allSensorType, // 后续新增的开关量的特殊业务,用来处理开关量业务 }} /> )} </div> </> ); } if (model === 'table') { return tableMemo; } }; // 获取字段配置 const getDefaultOptions = async () => { // 特定设备 // 这部分功能有问题,等待解决后上线 2024年3月13日 getDictionaryInfoAll({ level: '离散算法设备类型', }).then(res => { if (res.code === 0 && res.data.length) { let deviceType = res.data.find(item => item.fieldName === '设备类型')?.fieldValue; setDiscreteDeviceType(deviceType.split(',').filter(item => item)) } }) // 非单曲线、单指标不执行 if ( deviceParams?.length !== 1 || (deviceParams?.length === 1 && deviceParams?.[0]?.sensors?.split(',')?.length > 1) ) return setCompleteInit(true); setLoading(true); const {deviceCode, deviceType, sensors} = deviceParams[0]; let _id = ( await getPointAddress({ code: deviceCode, }) )?.data?.[0]?.id; let _params = {}; if (_id) _params.versionId = _id; // 多曲线的居中,容易导致曲线被截断,故多曲线时,不请求 let _request0 = getDictionaryInfoAll({ level: '组件_ec_historyview', }); // 以下请求为处理状态值、开关值的图表,只允许单曲线单指标情况下展示 let _request1 = getPointAddressEntry(_params); let _request2 = getSensorType(); // let _request3 = getPredicateSensor({deviceCode, sensors}); await Promise.all([_request0, _request1, _request2]).then((result) => { if (result) { let _res0 = result[0]; let _res1 = result[1]; let _res2 = result[2]; // let _res3 = result[3]; let _checkboxData = [...checkboxData]; // 单设备单曲线时,查询是否配置为预测点 /* if (_res3.code === 0 && _res3.data) { // 1. 如果是单曲线,并且配置了预测,那么默认开启预测; // 2024年3月11日 物联预测功能支撑后,再开发这部分 _checkboxData.push({ key: 'predicate', label: '数据预测', checked: true, showInCurve: true, showInTable: true, }) setPredicateDevice({..._res3.data, deviceType: '预测'}); } else { setPredicateDevice(null); }*/ // 查字典配置 if (_res0.code === 0) { let _opt = _res0.data.reduce((final, cur) => { final[cur.fieldName] = cur.fieldValue; return final; }, {}); _checkboxData = _checkboxData.map((item) => { let _item = {...item}; if (_opt[item.label] !== undefined) { _item.checked = _opt[item.label] === 'true'; } return _item; }); setCheckboxData(_checkboxData); } // 查点表配置 if (_res1.code === 0) { let _sensorConfig = _res1.data.find((item) => item?.name.trim() === sensors.trim()); let _statusName = _sensorConfig?.statusName; setAllPointAddress(_res1.data); if (_statusName) { let _statusConfig = _res1.data.find((item) => item?.name.trim() === _statusName.trim()); let _valDesc = _statusConfig?.valDesc || ''; setSpecial1({ name: _statusName, valDesc: _valDesc.split(';').reduce((final, cur) => { let _arr = cur.split(':'); final[_arr[0]] = _arr[1]; return final; }, {}), }); } } // 标记sensor是什么类型的 if (_res2.code === 0) { setAllSensorType(_res2.data); let _sensorID = _res1.data?.find((item) => item.name === sensors)?.sensorTypeID; let _sensor = _res2.data?.find((item) => item.id === _sensorID)?.type; let _isStatusSensor = ['状态值', '开关值'].includes(_sensor); setIsSingleStatusSensor(_isStatusSensor); } } }); setCompleteInit(true); }; useEffect(() => { getDefaultOptions(); }, [deviceParams]); let percentTimer = useRef({ timer: null, }); // 加载动画 useEffect(() => { if (loading === null) return; if (loading) { let _percent = percent; percentTimer.current.timer = setInterval(() => { _percent += 5; if (_percent > 95) return clearInterval(percentTimer.current.timer); setPercent(_percent); }, 100); } else { clearInterval(percentTimer.current.timer); setPercent(100); setTimeout( () => { setPercent(0); }, lineDataType === '原始曲线' ? 500 : 0, ); } }, [loading]); return ( <div className={classNames(prefixCls, theme === 'BI' ? BIStyles.historyViewComponents : '', 'wkt-scroll-light')} style={{background: theme === 'BI' ? '#282b34' : '#ffffff'}}> <div className={classNames(`${prefixCls}-spin`)} style={{position: 'relative'}}> {loading || percent !== 0 ? ( <div className={classNames(`${prefixCls}-progressWrapper`)}> {lineDataType === '原始曲线' || (lineDataType === '特征曲线' && moment(dateRange?.[0]?.dateTo).diff(moment(dateRange?.[0]?.dateFrom), 'days') >= 30) ? <div className={classNames(`${prefixCls}-contentWrapper`)}> <Progress percent={percent} steps={20} className={classNames(`${prefixCls}-progress`, `${prefixCls}-blink-2`)} showInfo={false} /> <div className={classNames(`${prefixCls}-tip`)}>加载中...</div> </div> : <Spin spinning={loading || false} tip={'数据加载中...'} delay={1000} style={{background: 'transparent'}}/> } </div> ) : ( '' )} {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={exportFeatureBtn}> <DownloadOutlined/> 下载 </Button> </> )} </div> ), }} > <Tabs.TabPane key="curve" tab="曲线" forceRender={true}> {activeTabKey === 'curve' ? renderPanel('curve') : ''} </Tabs.TabPane> <Tabs.TabPane key="table" tab="表格"> {renderPanel('table')} </Tabs.TabPane> </Tabs> )} </div> </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'])), defaultDate: PropTypes.string, BIMode: PropTypes.bool }; HistoryView.defaultProps = { grid: false, title: '指标曲线', defaultChecked: 'roundClock', tableProps: {}, defaultModel: 'curve', showModels: ['curve', 'table'], needMarkLine: true, defaultDate: 'day', BIMode: false }; export default HistoryView;