| | |
| | | import { is, fromJS } from 'immutable' |
| | | import { Chart, registerShape } from '@antv/g2' |
| | | import { Spin, notification } from 'antd' |
| | | import { DownloadOutlined } from '@ant-design/icons' |
| | | import moment from 'moment' |
| | | |
| | | import Api from '@/api' |
| | | import Utils from '@/utils/utils.js' |
| | | import asyncComponent from '@/utils/asyncComponent' |
| | | import UtilsDM from '@/utils/utils-datamanage.js' |
| | | import MKEmitter from '@/utils/events.js' |
| | | import NormalHeader from '@/tabviews/custom/components/share/normalheader' |
| | | import './index.scss' |
| | | |
| | | const NormalHeader = asyncComponent(() => import('@/tabviews/custom/components/share/normalheader')) |
| | | |
| | | registerShape('point', 'pointer', { |
| | | draw(cfg, container) { |
| | |
| | | }, |
| | | }) |
| | | |
| | | registerShape('point', 'hidden', { |
| | | draw(cfg, container) { |
| | | return container.addGroup({}) |
| | | } |
| | | }) |
| | | |
| | | class DashboardChart extends Component { |
| | | static propTpyes = { |
| | | BID: PropTypes.any, // 父级Id |
| | |
| | | title: '', // 组件标题 |
| | | sync: false, // 是否统一请求数据 |
| | | plot: null, // 图表设置 |
| | | data: null, // 数据 |
| | | data: {}, // 数据 |
| | | chart: null |
| | | } |
| | | |
| | | UNSAFE_componentWillMount () { |
| | | const { config, data, initdata, BID } = this.props |
| | | let _config = fromJS(config).toJS() |
| | | |
| | | let _data = {} |
| | | let _data = null |
| | | let _sync = _config.setting.sync === 'true' |
| | | |
| | | |
| | | if (_sync && data) { |
| | | _data = data[_config.dataName] || {} |
| | | if (_data && Array.isArray(_data)) { |
| | | _data = _data[0] || {} |
| | | } |
| | | _data = data[config.dataName] || [] |
| | | _sync = false |
| | | } else if (_sync && initdata) { |
| | | _data = initdata || {} |
| | | if (_data && Array.isArray(_data)) { |
| | | _data = _data[0] || {} |
| | | } |
| | | _data = initdata || [] |
| | | _sync = false |
| | | } |
| | | |
| | | let height = config.plot.height || 400 |
| | | if (config.plot.title) { |
| | | _config.plot.height = height - 75 |
| | | if (_config.subtype === 'ratioboard') { |
| | | _data = _data || [] |
| | | } else { |
| | | _config.plot.height = height - 30 |
| | | if (_data && Array.isArray(_data)) { |
| | | _data = _data[0] || {} |
| | | } else { |
| | | _data = {} |
| | | } |
| | | } |
| | | _config.style.height = height |
| | | |
| | | _config.style.height = config.plot.height || 400 |
| | | |
| | | this.setState({ |
| | | config: _config, |
| | |
| | | title: config.plot.title |
| | | }, () => { |
| | | if (config.setting.sync !== 'true' && config.setting.onload === 'true') { |
| | | this.loadData() |
| | | } else if (config.setting.sync === 'true' && _data) { |
| | | setTimeout(() => { |
| | | this.loadData() |
| | | }, _config.setting.delay || 0) |
| | | } else if (config.setting.sync === 'true') { |
| | | this.handleData() |
| | | } |
| | | }) |
| | |
| | | const { sync, config } = this.state |
| | | |
| | | if (sync && !is(fromJS(this.props.data), fromJS(nextProps.data))) { |
| | | let _data = {} |
| | | if (nextProps.data && nextProps.data[config.dataName]) { |
| | | _data = nextProps.data[config.dataName] || {} |
| | | } |
| | | if (_data.hasOwnProperty(config.plot.valueField)) { |
| | | _data.value = _data[config.plot.valueField] |
| | | let _data = null |
| | | |
| | | if (config.subtype === 'ratioboard') { |
| | | _data = [] |
| | | if (nextProps.data && nextProps.data[config.dataName]) { |
| | | _data = nextProps.data[config.dataName] || [] |
| | | } |
| | | } else { |
| | | _data = {} |
| | | if (nextProps.data && nextProps.data[config.dataName]) { |
| | | _data = nextProps.data[config.dataName] || {} |
| | | } |
| | | if (_data.hasOwnProperty(config.plot.valueField)) { |
| | | _data.value = _data[config.plot.valueField] |
| | | } |
| | | } |
| | | |
| | | this.setState({sync: false, data: _data}, () => { |
| | | this.handleData() |
| | | }) |
| | | } else if (nextProps.mainSearch && !is(fromJS(this.props.mainSearch), fromJS(nextProps.mainSearch))) { |
| | | if (config.setting.syncRefresh === 'true') { |
| | | this.setState({}, () => { |
| | | this.loadData() |
| | | }) |
| | | } |
| | | } else if (config.setting.syncRefresh && nextProps.mainSearch && !is(fromJS(this.props.mainSearch), fromJS(nextProps.mainSearch))) { |
| | | this.setState({}, () => { |
| | | this.loadData() |
| | | }) |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | if (!config.timer) return |
| | | |
| | | const _change = { '15s': 15000, '30s': 30000, '1min': 60000, '5min': 300000, '10min': 600000, '15min': 900000, '30min': 1800000, '1hour': 3600000 } |
| | | const _change = { '5s': 5000, '15s': 15000, '30s': 30000, '1min': 60000, '5min': 300000, '10min': 600000, '15min': 900000, '30min': 1800000, '1hour': 3600000 } |
| | | |
| | | let timer = _change[config.timer] |
| | | |
| | |
| | | const { config } = this.state |
| | | |
| | | if (!config.setting.supModule || config.setting.supModule !== MenuID) return |
| | | if (id !== this.state.BID) { |
| | | if (id !== this.state.BID || id !== '') { |
| | | this.setState({ BID: id }, () => { |
| | | this.loadData() |
| | | }) |
| | |
| | | if (_element) { |
| | | _element.innerHTML = '' |
| | | } |
| | | this.dashboardrender() |
| | | this.viewrender() |
| | | } |
| | | |
| | | async loadData (hastimer) { |
| | |
| | | return |
| | | } |
| | | |
| | | let searches = [] |
| | | if (mainSearch && mainSearch.length > 0) { // 主表搜索条件 |
| | | let keys = [] |
| | | mainSearch.forEach(item => { |
| | | if (!keys.includes(item.key)) { |
| | | searches.push(item) |
| | | } |
| | | }) |
| | | } |
| | | let searches = config.setting.useMSearch && mainSearch ? mainSearch : [] |
| | | |
| | | let requireFields = searches.filter(item => item.required && (!item.value || item.value.length === 0)) |
| | | let requireFields = searches.filter(item => item.required && item.value === '') |
| | | if (requireFields.length > 0) { |
| | | this.setState({ |
| | | loading: false |
| | | }) |
| | | return |
| | | } |
| | | |
| | |
| | | |
| | | let result = await Api.genericInterface(param) |
| | | if (result.status) { |
| | | let data = {} |
| | | if (result.data && result.data[0] && result.data[0].hasOwnProperty(config.plot.valueField)) { |
| | | data.value = result.data[0][config.plot.valueField] |
| | | let data = null |
| | | if (config.subtype === 'ratioboard') { |
| | | data = result.data || [] |
| | | } else { |
| | | data = {} |
| | | if (result.data && result.data[0] && result.data[0].hasOwnProperty(config.plot.valueField)) { |
| | | data.value = result.data[0][config.plot.valueField] |
| | | } |
| | | } |
| | | let reset = true |
| | | |
| | | if (hastimer && is(fromJS(data), fromJS(this.state.data))) { |
| | | reset = false |
| | | } |
| | | |
| | | this.setState({ |
| | | data, |
| | | loading: false |
| | | }, () => { |
| | | if (!reset) return |
| | | this.handleData() |
| | | }) |
| | | } else { |
| | |
| | | } |
| | | } |
| | | |
| | | viewrender = () => { |
| | | const { config } = this.state |
| | | if (config.subtype === 'ratioboard') { |
| | | this.ratioboardrender() |
| | | } else { |
| | | this.dashboardrender() |
| | | } |
| | | } |
| | | |
| | | getratiodata = () => { |
| | | const { data, plot } = this.state |
| | | |
| | | let colors = {} |
| | | if (plot.colors && plot.colors.length > 0) { |
| | | plot.colors.forEach(item => { |
| | | colors[item.tick] = item.color |
| | | }) |
| | | } |
| | | |
| | | return data.map(item => { |
| | | let val = +item[plot.valueField] |
| | | let type = item[plot.labelField] || '' |
| | | if (isNaN(val)) { |
| | | val = 0 |
| | | } |
| | | return { |
| | | type: type, |
| | | value: val, |
| | | $percent: val / plot.maxValue, |
| | | $color: colors[type] || '#1890ff' |
| | | } |
| | | }) |
| | | } |
| | | |
| | | ratioboardrender = () => { |
| | | const { plot, chartId } = this.state |
| | | const data = this.getratiodata() |
| | | |
| | | const chart = new Chart({ |
| | | container: chartId, |
| | | autoFit: true, |
| | | height: this.wrap.offsetHeight - 30, |
| | | }) |
| | | |
| | | chart.data(data) |
| | | chart.coordinate('polar', { |
| | | startAngle: -Math.PI / 2, |
| | | endAngle: 3 * Math.PI / 2, |
| | | radius: (plot.radius || 75) / 100 |
| | | }) |
| | | chart.scale('$percent', { |
| | | min: 0, |
| | | max: 1, |
| | | tickInterval: 1, |
| | | }) |
| | | chart.axis(false) |
| | | chart.facet('rect', { |
| | | fields: ['type'], |
| | | showTitle: false, |
| | | eachView: function eachView(view, facet) { |
| | | const data = facet.data[0] |
| | | |
| | | view.point().position('').shape('hidden') |
| | | |
| | | view.annotation().arc({ |
| | | top: false, |
| | | start: [0, 1], |
| | | end: [0.9999, 1], |
| | | style: { |
| | | stroke: plot.backColor, |
| | | lineWidth: 10 |
| | | } |
| | | }) |
| | | |
| | | let _tick = data.$percent |
| | | if (_tick >= 1) { |
| | | _tick = 0.9999 |
| | | } |
| | | |
| | | view.annotation().arc({ |
| | | start: [0, 1], |
| | | end: [_tick, 1], |
| | | style: { |
| | | stroke: data.$color, |
| | | lineWidth: 10, |
| | | } |
| | | }) |
| | | // 仪表盘信息 |
| | | let text = '' |
| | | if (plot.percent === 'true') { |
| | | text = +(data.$percent * 100).toFixed(2) + '%' |
| | | } else { |
| | | text = data.value |
| | | } |
| | | |
| | | view.annotation().text({ |
| | | position: ['50%', '45%'], |
| | | content: data.type, |
| | | style: { |
| | | fontSize: plot.fontSize * 0.8, |
| | | fill: plot.labelColor, |
| | | fontWeight: 300, |
| | | textAlign: 'center' |
| | | }, |
| | | offsetX: 0 |
| | | }) |
| | | view.annotation().text({ |
| | | position: ['50%', '55%'], |
| | | content: text, |
| | | style: { |
| | | fontSize: plot.fontSize, |
| | | fill: plot.labelColor, |
| | | fontWeight: 500, |
| | | textAlign: 'center' |
| | | }, |
| | | offsetX: 0, |
| | | offsetY: 10 |
| | | }) |
| | | } |
| | | }) |
| | | chart.render() |
| | | |
| | | this.setState({chart}) |
| | | } |
| | | |
| | | /** |
| | | * @description 仪表盘渲染 |
| | | */ |
| | |
| | | const { plot, chartId, data } = this.state |
| | | |
| | | let _data = fromJS(data).toJS() |
| | | if (_data.value && _data.value > plot.maxValue) { |
| | | _data.value = plot.maxValue |
| | | |
| | | if (_data.hasOwnProperty('value')) { |
| | | if (_data.value === '' || _data.value === null) { |
| | | delete _data.value |
| | | } else { |
| | | _data.value = +_data.value |
| | | |
| | | if (isNaN(_data.value)) { |
| | | delete _data.value |
| | | } else if (_data.value > plot.maxValue) { |
| | | _data.value = plot.maxValue |
| | | } |
| | | } |
| | | } |
| | | |
| | | const chart = new Chart({ |
| | | container: chartId, |
| | | autoFit: true, |
| | | height: plot.height, |
| | | height: this.wrap.offsetHeight - 30, |
| | | padding: [0, 0, 0, 0], |
| | | }) |
| | | chart.data([_data]); |
| | | chart.data([_data]) |
| | | chart.scale('value', { |
| | | min: 0, |
| | | max: plot.maxValue, |
| | |
| | | radius: 0.75, |
| | | }) |
| | | |
| | | chart.axis('1', false); |
| | | chart.axis('1', false) |
| | | chart.axis('value', { |
| | | line: null, |
| | | label: { |
| | |
| | | } |
| | | |
| | | chart.render() |
| | | |
| | | this.setState({chart}) |
| | | } |
| | | |
| | | downloadImage = () => { |
| | | const { chart, config } = this.state |
| | | const link = document.createElement('a'); |
| | | const filename = `${config.name}${moment().format('YYYY-MM-DD HH_mm_ss')}.png`; |
| | | const canvas = chart.getCanvas(); |
| | | canvas.get('timeline').stopAllAnimations(); |
| | | |
| | | setTimeout(() => { |
| | | const canvas = chart.getCanvas(); |
| | | const canvasDom = canvas.get('el'); |
| | | const dataURL = canvasDom.toDataURL('image/png'); |
| | | |
| | | if (window.Blob && window.URL) { |
| | | const arr = dataURL.split(','); |
| | | const mime = arr[0].match(/:(.*?);/)[1]; |
| | | const bstr = atob(arr[1]); |
| | | let n = bstr.length; |
| | | const u8arr = new Uint8Array(n); |
| | | while (n--) { |
| | | u8arr[n] = bstr.charCodeAt(n); |
| | | } |
| | | const blobObj = new Blob([u8arr], { type: mime }); |
| | | if (window.navigator.msSaveBlob) { |
| | | window.navigator.msSaveBlob(blobObj, filename); |
| | | } else { |
| | | link.addEventListener('click', () => { |
| | | link.download = filename; |
| | | link.href = window.URL.createObjectURL(blobObj); |
| | | }); |
| | | } |
| | | } |
| | | const e = document.createEvent('MouseEvents'); |
| | | e.initEvent('click', false, false); |
| | | link.dispatchEvent(e); |
| | | }, 16); |
| | | } |
| | | |
| | | render() { |
| | | const { config, loading, empty } = this.state |
| | | const { config, loading } = this.state |
| | | |
| | | return ( |
| | | <div className="custom-dashboard-plot-box" style={config.style}> |
| | |
| | | </div> : null |
| | | } |
| | | <NormalHeader config={config} /> |
| | | <div className="canvas-wrap"> |
| | | <div className={'canvas' + (empty ? ' empty' : '')} style={{height: config.plot.height + 30}} id={this.state.chartId}></div> |
| | | <div className="canvas-wrap" ref={ref => this.wrap = ref}> |
| | | {config.plot.download === 'enable' && this.state.chart ? <DownloadOutlined onClick={this.downloadImage} className="system-color download"/> : null} |
| | | <div className="canvas" id={this.state.chartId}></div> |
| | | </div> |
| | | </div> |
| | | ) |