import { reportService } from '../../../api'; import { LeftOutlined } from '@ant-design/icons'; import { Button, Form, Input, InputNumber, message, notification, Radio, Row, Select, Slider, Switch, Space, Tooltip, } from 'antd'; import { QuestionCircleOutlined } from '@ant-design/icons'; import classNames from 'classnames'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { BasicChart } from '@wisdom-components/basicchart'; import styles from './index.less'; import { OthersFormItems, TableLayoutFormItems, } from './forms'; import { defaultLayoutOptions, defaultChartOptions, handleCustomConfig, handleOthers, handleSeries, handleTitle, defaultConfig, } from './utils'; import { returnFields, returnOptionsArr } from '../../utils/handleOption'; import ReportsManage from '../../ReportsManage'; import { cloneDeep } from 'lodash'; const layoutOptions = [ { label: '上图下表', value: '上图下表', imgSrc: '' }, { label: '上表下图', value: '上表下图', imgSrc: '' }, { label: '左表右图', value: '左表右图', imgSrc: '' }, { label: '左图右表', value: '左图右表', imgSrc: '' }, ]; const verticalLayout = ['上表下图', '上图下表']; const horizontalLayout = ['左表右图', '左图右表']; const markPointOption = [ { label: '最大值', value: 'max' }, { label: '最小值', value: 'min' }, { label: '平均值', value: 'average' }, ]; const { Option } = Select; const defaultTableWidth = 100; const defaultTableHeight = 50; const defaultChartListConfig = [ { key: '图形1', }, ]; const ChartConfig = (props) => { const { setCurrentPage, currentReport } = props; const currentReportId = currentReport?.id ?? 0; const [layoutType, setLayoutType] = useState('上表下图'); const [chartNum, setChartNum] = useState(1); const [chartList, setChartList] = useState([]); const [tableWidth, setTableWidth] = useState(defaultTableWidth); const [tableHeight, setTableHeight] = useState(defaultTableHeight); const [configData, setConfigData] = useState([]); const [curChartIndex, setCurChartIndex] = useState(0); const [curChart, setCurChart] = useState(defaultChartListConfig); // 默认第一个图形 // const [xDataType, setXDataType] = useState('selectedData'); // x轴坐标 const [xDataType, setXDataType] = useState(''); // x轴坐标 const [columnsData, setColumnsData] = useState([]); const [reportData, setReportData] = useState([]); const [optionsList, setOptionsList] = useState([]); // const [chartType, setChartType] = useState(''); const [curId, setCurId] = useState(''); const [isEditing, setIsEditing] = useState(false); const [tableForm] = Form.useForm(null); const [chartForm] = Form.useForm(null); // const seriesData = Form.useWatch('seriesData', chartForm); const [seriesData, setSeriesData] = useState([]); const xAxisData = Form.useWatch('xAxisData', chartForm); const width = Form.useWatch('width', chartForm); const height = Form.useWatch('height', chartForm); const chartType = Form.useWatch('chartType', chartForm); const seriesArray = Form.useWatch('seriesArray', chartForm); const reportRef = useRef(); const jsonDataRef = useRef({ optionConsturctor: { layoutOptions: { layout: layoutType, chartCount: chartNum, tableConfigs: [], chartConfigs: [], }, chartOptions: [], }, }); const trigger = (str) => { let _data = reportRef?.current?.getData(); setReportData(_data); }; const calculateMaxValue = (key, arr, current) => { let _maxValue = 100; let _value = arr.filter(item => item.key !== current.key).map(item => { return Number(item.options.custom_style[key].replace('%', '')); }).reduce((final, cur) => (final + cur), 0); return _maxValue - _value; }; const returnChartOptions = async (chartList) => { let columns = returnFields(JSON.stringify(chartList)); let _data = []; // 如果没有columns表明是初始化时,无需请求数据,此时图表渲染出来的话,会是空白 if (columns.length) { let _request = await reportService.getChartConfigDataByColumn({ reportName: currentReport.reportName, columns }); if (_request.code === 0) { _data = _request.data.data; } } let _keys = chartList.map(item => item.key); let _options = chartList.map(item => item.options); let _opts = returnOptionsArr(_options, _data, reportData.selectedRows); setOptionsList(_opts.map((item, index) => ({ key: _keys[index], options: item }))); }; useEffect(() => { let _list = chartList.filter(item => item.options && item.options.series); if (_list.length) { returnChartOptions(_list); } }, [chartList]); // 图容器 const renderChartWrap = useMemo(() => { return ( <div className={styles.chartWrapper}> {optionsList.map((item, index) => ( <div className={classNames( styles.chartItem, curChartIndex === index ? styles.activeChart : '', )} key={item.key} onClick={() => clickChart(chartList[index], index)} style={{ width: item?.options?.custom_style?.width ?? 'auto', height: item?.options?.custom_style?.height ?? 'auto', }} > { item.options ? <div style={{ width: '100%', height: '100%', pointerEvents: 'none' }}> <BasicChart option={item.options}/> </div> : item.key } </div> ))} </div> ); }, [curChartIndex, chartList, optionsList, layoutType, chartNum]); // 表格容器 const renderTableWrap = useMemo(() => { return ( <div className={styles.tableWrapper} style={{ width: tableWidth, height: tableHeight, }} > {/*图表1*/} <ReportsManage params={{ reportName: currentReport.reportName, customState: [] }} style={{ width: '100%', height: '100%' }} ref={reportRef} trigger={trigger}/> </div> ); }, [tableWidth, tableHeight, currentReport]); // 渲染左侧布局 const renderLayout = () => { switch (layoutType) { case '上图下表': return ( <div className={classNames(styles.layoutWrapper, styles.columnLayout)} > {renderChartWrap} {renderTableWrap} </div> ); case '上表下图': return ( <div className={classNames(styles.layoutWrapper, styles.columnLayout)} > {renderTableWrap} {renderChartWrap} </div> ); case '左表右图': return ( <div className={classNames(styles.layoutWrapper, styles.rowLayout)}> {renderTableWrap} {renderChartWrap} </div> ); case '左图右表': return ( <div className={classNames(styles.layoutWrapper, styles.rowLayout)}> {renderChartWrap} {renderTableWrap} </div> ); } }; // 点击单个chart const clickChart = (item, index) => { setCurChartIndex(index); setCurChart(item); //更新右侧面板form表单数据 updateFormData(item.options); }; // 更新表单数据 const updateFormData = (data) => { // 如果data不存在,可能是未配置的图形,此时需要重置表单 if (!data) { chartForm.resetFields(); // setChartType(''); setXDataType('selectedData'); } const _xDataType = data.custom_config.renderBy; const _xAxisData = _xDataType === 'selectedData' ? data.xAxis.data : extraText(data.xAxis.data); const _seriesData = data.series.map(item => { let _str = extraText(item.data); return _str; }); const _seriesArray = data.series.map(item => { return { chartType: item.type, // chartName: item.name, unit: item.custom_config.unit, markPoint: item.markPoint.data, markLine: item.markLine.data, }; }); setXDataType(_xDataType); setSeriesData(_seriesData); chartForm.setFieldsValue({ width: data.custom_style.width.match(/\d+/)?.[0], height: data.custom_style.height.match(/\d+/)?.[0], xAxisType: data.xAxis.type, xDataType: _xDataType, xAxisData: _xAxisData?.split(',') || [], xAxisName: data.xAxis.name, // barWidth: // seriesData: _seriesData, seriesArray: _seriesArray, // chartColor: data.color, showTitle: data.title.show, text: data.title.text, fontSize: data.title.textStyle.fontSize, fontColor: data.title.textStyle.color, yAxisName: data.yAxis[0].name, left: data.grid.left, right: data.grid.right, top: data.grid.top, bottom: data.grid.bottom, showDataZoom: data.dataZoom.show, legendPosition: data.legend.left, }); }; // 处理字符串 提取纯文字 const extraText = (str) => { if (!str) return void 0; const text = str.slice(2, str.length - 1); return text; }; // 图形数量 const onChangeNum = (e) => { const num = e.target.value; // 判断num是否大于chartList的长度,大于则赋值add,小于则赋值delete const type = num > chartList.length ? 'add' : 'delete'; let newArr = []; if (type === 'add') { newArr = chartList.concat(); for (let i = 0; i < num - chartList.length; i++) { let _defaultOptions = cloneDeep(defaultChartOptions); newArr.push({ key: `图形${chartList.length + i + 1}`, options: _defaultOptions, }); } } if (type === 'delete') { newArr = chartList.slice(0, num); } newArr = newArr.map((item, index) => { if (num === 1) { item.options.custom_style = { width: '100%', height: '100%', }; } if (num === 2) { item.options.custom_style = { width: verticalLayout.includes(layoutType) ? '50%' : '100%', height: horizontalLayout.includes(layoutType) ? '50%' : '100%', }; } if (num === 3) { if (index === 0) { item.options.custom_style = { width: verticalLayout.includes(layoutType) ? '34%' : '100%', height: horizontalLayout.includes(layoutType) ? '34%' : '100%', }; } else { item.options.custom_style = { width: verticalLayout.includes(layoutType) ? '33%' : '100%', height: horizontalLayout.includes(layoutType) ? '33%' : '100%', }; } } return item; }); setChartList(newArr); setChartNum(num); }; // 表格表单字段变化 const onTableFormChange = (changedValues, allValues) => { const field = Object.keys(changedValues)[0]; const fieldValue = changedValues[field]; switch (field) { case 'width': setTableWidth(fieldValue + '%'); break; case 'height': setTableHeight(fieldValue + '%'); break; default: break; } }; // 图形表单字段变化 const onChartFormChange = (changedValues, allValues, index) => { const field = Object.keys(changedValues)[0]; const fieldValue = changedValues[field]; if (field === 'xDataType') { setXDataType(fieldValue); } if (field === 'chartType') { // setChartType(fieldValue); } if (field === 'showDataZoom' && fieldValue) { chartForm.setFieldsValue({ bottom: 40 }); } }; // 提交 const handleOptions = (tableFormData) => { const _chartOptions = chartList .filter((item) => !!item.options) .map((v) => v.options); // 表格配置数据 const tableConfigs = [ { title: tableFormData.title, width: tableFormData.width + '%', height: tableFormData.height + '%', }, ]; jsonDataRef.current.optionConsturctor.layoutOptions = { layout: layoutType, chartCount: chartNum, tableConfigs: tableConfigs, }; jsonDataRef.current.optionConsturctor.chartOptions = _chartOptions; return JSON.stringify(jsonDataRef.current.optionConsturctor); }; const onSubmit = async () => { // 表格相关配置 const tableFormData = await tableForm.validateFields(); let _data = handleOptions(tableFormData); saveConfig(_data); }; // 调用保存接口 const saveConfig = async (jsonData) => { const data = { id: curId || undefined, reportID: currentReportId, configInfo: jsonData, }; const res = await reportService.saveChartConfig(data); if (res.code === 0) { message.success('配置成功!'); getChartConfig(); } else { notification.error({ message: `请求错误`, description: res?.msg ?? '', }); } }; useEffect(() => { if (chartList.length) { saveFormData('modify'); } }, [width, height]); // 保存当前图形form表单数据 const saveFormData = async (type) => { let form = {}; if (type === 'save') form = await chartForm.validateFields(); if (type === 'modify') form = await chartForm.getFieldsValue(); // x轴数据 格式如下: // selectedData: [data1,data2...] // allData: ${data1} const _title = handleTitle(form); const _series = handleSeries(form, seriesData, markPointOption); const _others = handleOthers(form); const _custom = handleCustomConfig(form); const xData = form.xDataType === 'selectedData' ? form.xAxisData : (form.xAxisData ? '${' + form.xAxisData + '}' : ''); const _options = { ..._custom, ..._title, ..._series, ..._others, xAxis: { name: form?.xAxisName ?? '', type: form.xAxisType, data: xData, }, yAxis: form?.yAxisName2 ?? '' ? [ { name: form?.yAxisName ?? '', gridIndex: 0, }, { name: form?.yAxisName2 ?? '', gridIndex: 1, }, ] : [ { name: form?.yAxisName ?? '', gridIndex: 0, }, ], }; const newData = [...chartList]; newData[curChartIndex].options = _options; setChartList(newData); }; const getChartConfig = async () => { const res = await reportService.getChartConfig({ reportName: currentReport.reportName, }); if (res.code === 0) { const data = res?.data?.configInfo ? JSON.parse(res.data.configInfo) : cloneDeep(defaultConfig); let { chartOptions, layoutOptions } = data; // 无设置,即初始化时,给定默认配置 layoutOptions.tableConfigs[0].title = `${currentReport.reportName}`; // 有设置 const _chartList = chartOptions.map((item, index) => ({ options: item, key: '图形' + (index + 1), })); updateFormData(_chartList?.[0]?.options); setCurChartIndex(0); setCurId(res?.data?.id ?? ''); setCurChart(_chartList[0]); setChartList(_chartList); dealTableConfig(layoutOptions); setChartNum(layoutOptions.chartCount); } }; // 回填表格配置 const dealTableConfig = (data) => { const { layout, tableConfigs } = data; const tableWidth = tableConfigs[0].width; const tableHeight = tableConfigs[0].height; setTableWidth(tableWidth); setTableHeight(tableHeight); setLayoutType(layout); tableForm.setFieldsValue({ width: tableWidth.match(/\d+/)?.[0], height: tableHeight.match(/\d+/)?.[0], title: tableConfigs[0].title, }); }; // 获取配置数据 const getConfig = async () => { const params = { reportName: currentReport.reportName, }; const res = await reportService.getReportDetails(params); if (res.code === 0) { const data = (res?.data?.data || []).map((v) => ({ label: v.fieldAlias, value: v.fieldAlias, })); setConfigData(data); } }; const setTableSize = (e) => { const _map = { horizontal: { width: '100%', height: '70%', }, vertical: { width: '70%', height: '100%', }, }; let _obj = {}; if (horizontalLayout.includes(e)) { _obj = _map.horizontal; } else { _obj = _map.vertical; } // 设置报表宽度 let { width, height } = _obj; setTableHeight(height); setTableWidth(width); tableForm.setFieldsValue({ width: width.match(/\d+/)?.[0], height: height.match(/\d+/)?.[0], }); // 设置图表宽度 }; const widthMax = useMemo(() => { return verticalLayout.includes(layoutType) ? calculateMaxValue('width', chartList, curChart) : 100; }, [layoutType, chartList, curChart]); const heightMax = useMemo(() => { return verticalLayout.includes(layoutType) ? 100 : calculateMaxValue('height', chartList, curChart); }, []); const clickPanel = (e) => { if (!isEditing) { message.info('请点击编辑按钮后,进行编辑'); } }; // form其他props const formFrops = { labelAlign: 'right', labelCol: { span: 4, offset: 2 }, wrapperCol: { span: 14 }, }; const allDataForm = () => ( <> <Form.Item label="图形数据" rules={[ { required: true, message: '数据来源必填', }, ]} extra={returnSeriesExtra} > <Select value={seriesData} mode="multiple" onChange={(e) => { setSeriesData(e); }} allowClear options={configData} placeholder={'请选择图形数据来源字段'}/> </Form.Item> <Form.Item label="类型" name={'chartType'} rules={[ { required: true, message: '类型必选', }, ]} > <Select> <Option value="line">折线图</Option> <Option value="bar">柱状图</Option> <Option value="pie">饼图</Option> </Select> </Form.Item> {chartType === 'bar' && ( <> <Form.Item label="柱条宽度" name={'barWidth'}> <Slider/> </Form.Item> </> )} {/* <Form.Item label="图形名称" name={'chartName'} rules={[ { // required: true, message: '图形名称必填', }, ]} > <Input/> </Form.Item>*/} <Form.Item label="单位" name={'unit'}> <Input/> </Form.Item> <Form.Item label="标注点" name={'markPoint'}> <Select mode="multiple" allowClear options={markPointOption}/> </Form.Item> <Form.Item label="标注线" name={'markLine'}> <Select mode="multiple" allowClear options={markPointOption}/> </Form.Item> </> ); const returnSeriesExtra = useMemo(() => { if (seriesData?.length) { if (xDataType === 'selectedData') return seriesData?.length > 1 ? `图形的系列名称的格式为${seriesData.join('-')}` : `图形系列的名称为${seriesData.join('')}列的数据`; if (xDataType === 'allData') return seriesData?.length > 1 ? `图形将展示${seriesData.join(',')}列的数据,请分别配置属性` : `图形将展示${seriesData.join('')}列的数据`; } return ''; }, [seriesData, xDataType]); const returnXAxisExtra = useMemo(() => { if (xAxisData?.length) { if (xDataType === 'selectedData') return `x轴坐标分别为${xAxisData.join(',')}`; if (xDataType === 'allData') return xAxisData.length === 1 ? `x轴坐标为${xAxisData}列的值` : `x轴坐标为拼接选中${xAxisData.length}列的数据,形式为${xAxisData.join('-')}`; } return ''; }, [xAxisData]); useEffect(() => { if (currentReportId) { getChartConfig(); getConfig(); } }, []); return ( <div className={styles.chartConfig}> <Row className={styles.controlRow}> <LeftOutlined className={styles.leftBtn} onClick={() => { setCurrentPage('报表列表'); }} /> <Form layout={'inline'}> <Form.Item label={'图表布局'}> <Select options={layoutOptions} onChange={(e) => { setTableSize(e); setLayoutType(e); }} value={layoutType} /> </Form.Item> <Form.Item label={'图形数量'}> <Radio.Group onChange={onChangeNum} value={chartNum}> <Radio value={1}>1</Radio> <Radio value={2}>2</Radio> <Radio value={3}>3</Radio> </Radio.Group> </Form.Item> </Form> </Row> <div className={styles.contentWrapper}> <div className={styles.leftLayout}>{renderLayout()}</div> <div className={styles.rightPanel}> <div className={styles.configForm}> <Row className={styles.titleWrap}>表格配置</Row> <Form form={tableForm} {...formFrops} onValuesChange={onTableFormChange} className={styles.tableFormWrap} > <TableLayoutFormItems defaultTableWidth={defaultTableWidth} defaultTableHeight={defaultTableHeight}/> </Form> <Row className={styles.titleWrap}>{curChart.key}配置</Row> <Row className={styles.saveBtnWrap}> <Tooltip title={'点击按钮进入、退出编辑状态。请注意及时保存配置!'}><QuestionCircleOutlined/></Tooltip> <Button onClick={() => setIsEditing(!isEditing)}>{isEditing ? '退出编辑' : '编辑'}</Button> </Row> <Form form={chartForm} onValuesChange={(changedValues, allValues) => onChartFormChange(changedValues, allValues) } {...formFrops} className={classNames(styles.chartFormWrap, 'wkt-scroll-light')} > <div className={classNames(isEditing ? '' : styles.pointerEvents)}> <Row className={styles.commonTitle}>宽高配置</Row> <Form.Item label="宽度" name="width"> <InputNumber min={0} max={widthMax} disabled={horizontalLayout.includes(layoutType)}/> </Form.Item> <Form.Item label="高度" name="height"> <InputNumber min={0} max={heightMax} disabled={verticalLayout.includes(layoutType)}/> </Form.Item> <Row className={styles.commonTitle}>x轴配置</Row> <Form.Item label="x轴类型" name="xAxisType" initialValue={'category'} > <Radio.Group> <Radio value="category">分类、周期、个体等</Radio> <Radio value="time">时间</Radio> </Radio.Group> </Form.Item> <Form.Item label="x轴坐标" name="xDataType" initialValue={'selectedData'} rules={[ { required: true, message: '必选', }, ]} > <Radio.Group> <Radio value="allData">表内数据</Radio> <Radio value="selectedData">表头数据</Radio> </Radio.Group> </Form.Item> <Form.Item label="x轴数据源" name="xAxisData" rules={[ { required: true, message: 'x轴数据源必填', }, ]} extra={returnXAxisExtra} > <Select placeholder={'请勾选x轴需要的字段'} mode="multiple" allowClear options={configData}/> </Form.Item> <Form.Item label="轴名称" name="xAxisName"> <Input placeholder={'指定轴的名称,非必填'}/> </Form.Item> <Row className={styles.commonTitle}>图形配置</Row> { xDataType === 'allData' ? <Form.List name={'seriesArray'}> { (fields, { add, remove }) => ( <> <Form.Item label={<span><span style={{ color: 'red', marginRight: 4 }}>*</span>图形数据</span>} rules={[ { required: true, message: '数据来源必填', }, ]} extra={returnSeriesExtra} > <Select mode="multiple" value={seriesData} onSelect={() => add()} onDeselect={(e) => { let _index = seriesData.findIndex(list => list === e); remove(_index); }} onChange={(e) => { setSeriesData(e); }} allowClear options={configData} placeholder={'请选择图形数据来源字段'}/> </Form.Item> { fields.map((item, index) => ( <div key={`${item.key}_${index}`}> <Form.Item colon={false} label={<span style={{ fontWeight: 'bold' }}>{seriesData?.[index]}</span>}/> <Form.Item label="类型" name={[item.name, 'chartType']} rules={[ { required: true, message: '类型必选', }, ]} > <Select> <Option value="line">折线图</Option> <Option value="bar">柱状图</Option> <Option value="pie">饼图</Option> </Select> </Form.Item> <Form.Item hidden={seriesArray?.[index]?.chartType !== 'bar'} label="柱条宽度" name={[item.name, 'barWidth']}> <Slider/> </Form.Item> {/* <Form.Item style={{ pointerEvents: 'none' }} label="图形名称" name={[item.name, 'chartName']} rules={[ { // required: true, message: '图形名称必填', }, ]} > <Input/> </Form.Item>*/} <Form.Item label="单位" name={[item.name, 'unit']}> <Input/> </Form.Item> <Form.Item label="标注点" name={[item.name, 'markPoint']}> <Select mode="multiple" allowClear options={markPointOption}/> </Form.Item> <Form.Item label="标注线" name={[item.name, 'markLine']}> <Select mode="multiple" allowClear options={markPointOption}/> </Form.Item> </div> )) } </> ) } </Form.List> : '' } { xDataType === 'selectedData' ? <> {allDataForm()} </> : '' } <Row className={styles.commonTitle}>标题配置</Row> <Form.Item label="图表标题" name="showTitle" valuePropName="checked" > <Switch/> </Form.Item> {/*包含标题、y轴等非必要的设置*/} <OthersFormItems/> </div> </Form> </div> <div className={styles.submitWrap}> <Button style={{ marginLeft: 10 }} className={styles.submitBtn} type="primary" onClick={onSubmit} > 提交 </Button> <Button type="primary" onClick={() => saveFormData('save')}> 预览 </Button> </div> </div> </div> </div> ); }; export default ChartConfig;