🆕 新增数据可视化组件Chart,搭建可视化数据配置流程

This commit is contained in:
xujiang 2020-09-15 22:10:14 +08:00
parent 0858944c77
commit c81e15efaa
14 changed files with 847 additions and 361 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -50,6 +50,8 @@
},
"dependencies": {
"@ant-design/icons": "^4.2.1",
"@antv/f2": "^3.7.7",
"@types/node": "^14.6.2",
"@umijs/plugin-sass": "^1.1.1",
"@umijs/preset-react": "1.x",
"@umijs/test": "^3.2.19",

BIN
src/assets/chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,13 @@
.chartWrap {
position: relative;
width: 100%;
.chartTitle {
text-align: center;
}
img {
width: 100%;
}
canvas {
width: 100%;
}
}

View File

@ -0,0 +1,58 @@
import { Chart } from '@antv/f2';
import React, { memo, PropsWithChildren, useEffect, useRef } from 'react';
// import { uuid } from 'utils/tool';
import ChartImg from '@/assets/chart.png';
import styles from './index.less';
type DataItem = {
name: string;
value: number;
};
interface XChartProps {
isTpl: boolean;
title: string;
color: string;
size: number;
paddingTop: number;
data: Array<DataItem>;
}
const XChart = (props: PropsWithChildren<XChartProps>) => {
const { isTpl, data, color, size, paddingTop, title } = props;
const chartRef = useRef(null);
useEffect(() => {
if (!isTpl) {
const chart = new Chart({
el: chartRef.current || undefined,
pixelRatio: window.devicePixelRatio, // 指定分辨率
});
// step 2: 处理数据
const dataX = data.map(item => ({ ...item, value: Number(item.value) }));
// Step 2: 载入数据源
chart.source(dataX);
// Step 3创建图形语法绘制柱状图由 genre 和 sold 两个属性决定图形位置genre 映射至 x 轴sold 映射至 y 轴
chart
.interval()
.position('name*value')
.color('name');
// Step 4: 渲染图表
chart.render();
}
}, []);
return (
<div className={styles.chartWrap}>
<div className={styles.chartTitle} style={{ color, fontSize: size, paddingTop }}>
{title}
</div>
{isTpl ? <img src={ChartImg} alt="dooring chart" /> : <canvas ref={chartRef}></canvas>}
</div>
);
};
export default memo(XChart);

Binary file not shown.

View File

@ -1,6 +1,6 @@
import { BasicTemplateItem } from './schema';
export type GraphTplKeyType = 'XProgress';
export type GraphTplKeyType = 'XProgress' | 'Chart';
export type GraphTplType = Array<BasicTemplateItem<GraphTplKeyType>>;
const graphTpl: GraphTplType = [
@ -8,6 +8,10 @@ const graphTpl: GraphTplType = [
type: 'XProgress',
h: 102,
},
{
type: 'Chart',
h: 102,
},
];
export default graphTpl;

View File

@ -3,7 +3,7 @@ import Loading from '../LoadingCp';
import { useMemo, memo, FC } from 'react';
import React from 'react';
import { AllTemplateType } from './schema';
const needList = ['Tab', 'Carousel', 'Upload', 'Video', 'Icon'];
const needList = ['Tab', 'Carousel', 'Upload', 'Video', 'Icon', 'Chart'];
const DynamicFunc = (type: AllTemplateType) =>
dynamic({

View File

@ -16,6 +16,7 @@ export type BasicSchemaType =
| 'Select'
| 'MutiText'
| 'Upload'
| 'Table'
| 'CardPicker';
export type BasicTemplateItem<T> = {
type: T;
@ -427,6 +428,34 @@ export interface XProgressSchema extends SchemaBasicImplement {
config: XProgressConfigType;
}
//__________________________________________
//________________xchart________________________
export type XChartDataItem = {
name: string;
value: string | number;
};
export type XChartEditItem = {
key: string;
name: string;
type: BasicSchemaType;
range?: BasicRangeType<string>[] | number[];
};
export type XChartConfigType = {
title: string;
size: number;
color: string;
paddingTop: number;
data: Array<XChartDataItem>;
};
export interface XChartSchema extends SchemaBasicImplement {
editData: Array<XChartEditItem>;
config: XChartConfigType;
}
//__________________________________________
//________________SCHEMA________________________
@ -444,6 +473,7 @@ export interface SchemaType extends SchemaImplement {
Icon: IconSchema;
Video: VideoSchema;
XProgress: XProgressSchema;
Chart: XChartSchema;
}
const schema: SchemaType = {
@ -1195,6 +1225,55 @@ const schema: SchemaType = {
strokeWidth: 10,
},
},
Chart: {
editData: [
{
key: 'title',
name: '标题',
type: 'Text',
},
{
key: 'size',
name: '标题大小',
type: 'Number',
},
{
key: 'color',
name: '标题颜色',
type: 'Color',
},
{
key: 'paddingTop',
name: '上边距',
type: 'Number',
},
{
key: 'data',
name: '数据源',
type: 'Table',
},
],
config: {
title: '柱状图',
size: 14,
color: 'rgba(0,0,0,1)',
paddingTop: 10,
data: [
{
name: 'A',
value: 20,
},
{
name: 'B',
value: 60,
},
{
name: 'C',
value: 20,
},
],
},
},
};
export default schema;

View File

@ -5,6 +5,7 @@ import DataList from '../DataList';
import MutiText from '../MutiText';
import Color from '../Color';
import CardPicker from '../CardPicker';
import Table from '../Table';
import { Store } from 'antd/lib/form/interface';
import { BasicRangeType, IconSchema } from '../DynamicEngine/schema';
// import styles from './index.less';
@ -141,6 +142,11 @@ const FormEditor = (props: FormEditorProps) => {
/>
</Form.Item>
)}
{item.type === 'Table' && (
<Form.Item label={item.name} name={item.key} valuePropName="data">
<Table data={item.data} />
</Form.Item>
)}
</React.Fragment>
);
})}

View File

@ -0,0 +1,26 @@
:global(.editable-cell) {
position: relative;
}
:global(.editable-cell-value-wrap) {
padding: 5px 12px;
cursor: pointer;
}
:global(.editable-row) {
&:hover :global(.editable-cell-value-wrap) {
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 4px 11px;
}
}
:global([data-theme='dark']) {
:global(.editable-row) {
&:hover {
:global(.editable-cell-value-wrap) {
border: 1px solid #434343;
}
}
}
}

View File

@ -0,0 +1,248 @@
import React, { useContext, useState, useEffect, useRef, memo } from 'react';
import { Table, Input, Button, Popconfirm, Form, Modal } from 'antd';
// 下方样式主要为全局样式,暂时不可删
import styles from './index.less';
const EditableContext = React.createContext<any>();
interface Item {
key: string;
name: string;
age: string;
address: string;
}
interface EditableRowProps {
index: number;
}
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};
interface EditableCellProps {
title: React.ReactNode;
editable: boolean;
children: React.ReactNode;
dataIndex: string;
record: Item;
handleSave: (record: Item) => void;
}
const EditableCell: React.FC<EditableCellProps> = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef();
const form = useContext(EditableContext);
useEffect(() => {
if (editing) {
inputRef.current.focus();
}
}, [editing]);
const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
};
const save = async e => {
try {
const values = await form.validateFields();
toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};
let childNode = children;
if (editable) {
childNode = editing ? (
<Form.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[
{
required: true,
message: `${title} 是必填的.`,
},
]}
>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div className="editable-cell-value-wrap" style={{ paddingRight: 24 }} onClick={toggleEdit}>
{children}
</div>
);
}
return <td {...restProps}>{childNode}</td>;
};
class EditableTable extends React.Component {
constructor(props) {
super(props);
this.columns = [
{
title: '名字',
dataIndex: 'name',
width: '180px',
editable: true,
},
{
title: '值',
dataIndex: 'value',
width: '120px',
editable: true,
},
{
title: '操作',
dataIndex: 'operation',
render: (text: string, record) =>
this.state.dataSource.length >= 1 ? (
<Popconfirm title="Sure to delete?" onConfirm={() => this.handleDelete(record.key)}>
<a></a>
</Popconfirm>
) : null,
},
];
const dataSource = props.data.map((item, i: number) => ({ key: i + '', ...item }));
this.state = {
dataSource: dataSource,
count: 2,
visible: false,
};
}
handleDelete = (key: string) => {
const dataSource = [...this.state.dataSource];
const newDataSource = dataSource.filter(item => item.key !== key);
this.setState({ dataSource: newDataSource });
this.props.onChange && this.props.onChange(newDataSource);
};
handleAdd = () => {
const { count, dataSource } = this.state;
const newData = {
key: count,
name: `dooring ${count}`,
value: 32,
};
const newDataSource = [...dataSource, newData];
this.setState({
dataSource: newDataSource,
count: count + 1,
});
this.props.onChange && this.props.onChange(newDataSource);
};
handleSave = row => {
const newData = [...this.state.dataSource];
const index = newData.findIndex(item => row.key === item.key);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row,
});
this.setState({ dataSource: newData });
this.props.onChange && this.props.onChange(newData);
};
showModal = () => {
this.setState({
visible: true,
});
};
handleOk = e => {
console.log(e);
this.setState({
visible: false,
});
};
handleCancel = e => {
console.log(e);
this.setState({
visible: false,
});
};
render() {
const { dataSource } = this.state;
const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};
const columns = this.columns.map(col => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: record => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave: this.handleSave,
}),
};
});
return (
<div>
<Button type="primary" onClick={this.showModal}>
</Button>
<Modal
title="编辑数据源"
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
okText="确定"
cancelText="取消"
>
<Button onClick={this.handleAdd} type="primary" style={{ marginBottom: 16 }}>
</Button>
<Button onClick={this.handleAdd} type="primary" ghost>
Excel
</Button>
<Table
components={components}
rowClassName={() => 'editable-row'}
bordered
dataSource={dataSource}
columns={columns}
pagination={{ pageSize: 50 }}
scroll={{ y: 240 }}
/>
</Modal>
</div>
);
}
}
export default memo(EditableTable);

View File

@ -13,7 +13,7 @@ export default memo(function ZanPao() {
<div className={styles.takeCat}>
<Popover placement="top" title={null} content={content} trigger="hover">
<Button type="primary" danger>
🍵
</Button>
</Popover>
</div>

766
yarn.lock

File diff suppressed because it is too large Load Diff