mirror of
https://github.com/MrXujiang/h5-Dooring.git
synced 2026-02-05 17:19:19 +00:00
🆕 新增数据可视化组件Chart,搭建可视化数据配置流程
This commit is contained in:
parent
0858944c77
commit
c81e15efaa
@ -50,6 +50,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.2.1",
|
"@ant-design/icons": "^4.2.1",
|
||||||
|
"@antv/f2": "^3.7.7",
|
||||||
|
"@types/node": "^14.6.2",
|
||||||
"@umijs/plugin-sass": "^1.1.1",
|
"@umijs/plugin-sass": "^1.1.1",
|
||||||
"@umijs/preset-react": "1.x",
|
"@umijs/preset-react": "1.x",
|
||||||
"@umijs/test": "^3.2.19",
|
"@umijs/test": "^3.2.19",
|
||||||
|
|||||||
BIN
src/assets/chart.png
Normal file
BIN
src/assets/chart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
13
src/components/Chart/index.less
Normal file
13
src/components/Chart/index.less
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.chartWrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
.chartTitle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/components/Chart/index.tsx
Normal file
58
src/components/Chart/index.tsx
Normal 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);
|
||||||
BIN
src/components/DynamicEngine/.DS_Store
vendored
BIN
src/components/DynamicEngine/.DS_Store
vendored
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
import { BasicTemplateItem } from './schema';
|
import { BasicTemplateItem } from './schema';
|
||||||
|
|
||||||
export type GraphTplKeyType = 'XProgress';
|
export type GraphTplKeyType = 'XProgress' | 'Chart';
|
||||||
export type GraphTplType = Array<BasicTemplateItem<GraphTplKeyType>>;
|
export type GraphTplType = Array<BasicTemplateItem<GraphTplKeyType>>;
|
||||||
|
|
||||||
const graphTpl: GraphTplType = [
|
const graphTpl: GraphTplType = [
|
||||||
@ -8,6 +8,10 @@ const graphTpl: GraphTplType = [
|
|||||||
type: 'XProgress',
|
type: 'XProgress',
|
||||||
h: 102,
|
h: 102,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'Chart',
|
||||||
|
h: 102,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default graphTpl;
|
export default graphTpl;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Loading from '../LoadingCp';
|
|||||||
import { useMemo, memo, FC } from 'react';
|
import { useMemo, memo, FC } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AllTemplateType } from './schema';
|
import { AllTemplateType } from './schema';
|
||||||
const needList = ['Tab', 'Carousel', 'Upload', 'Video', 'Icon'];
|
const needList = ['Tab', 'Carousel', 'Upload', 'Video', 'Icon', 'Chart'];
|
||||||
|
|
||||||
const DynamicFunc = (type: AllTemplateType) =>
|
const DynamicFunc = (type: AllTemplateType) =>
|
||||||
dynamic({
|
dynamic({
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export type BasicSchemaType =
|
|||||||
| 'Select'
|
| 'Select'
|
||||||
| 'MutiText'
|
| 'MutiText'
|
||||||
| 'Upload'
|
| 'Upload'
|
||||||
|
| 'Table'
|
||||||
| 'CardPicker';
|
| 'CardPicker';
|
||||||
export type BasicTemplateItem<T> = {
|
export type BasicTemplateItem<T> = {
|
||||||
type: T;
|
type: T;
|
||||||
@ -427,6 +428,34 @@ export interface XProgressSchema extends SchemaBasicImplement {
|
|||||||
config: XProgressConfigType;
|
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________________________
|
//________________SCHEMA________________________
|
||||||
|
|
||||||
@ -444,6 +473,7 @@ export interface SchemaType extends SchemaImplement {
|
|||||||
Icon: IconSchema;
|
Icon: IconSchema;
|
||||||
Video: VideoSchema;
|
Video: VideoSchema;
|
||||||
XProgress: XProgressSchema;
|
XProgress: XProgressSchema;
|
||||||
|
Chart: XChartSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema: SchemaType = {
|
const schema: SchemaType = {
|
||||||
@ -1195,6 +1225,55 @@ const schema: SchemaType = {
|
|||||||
strokeWidth: 10,
|
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;
|
export default schema;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import DataList from '../DataList';
|
|||||||
import MutiText from '../MutiText';
|
import MutiText from '../MutiText';
|
||||||
import Color from '../Color';
|
import Color from '../Color';
|
||||||
import CardPicker from '../CardPicker';
|
import CardPicker from '../CardPicker';
|
||||||
|
import Table from '../Table';
|
||||||
import { Store } from 'antd/lib/form/interface';
|
import { Store } from 'antd/lib/form/interface';
|
||||||
import { BasicRangeType, IconSchema } from '../DynamicEngine/schema';
|
import { BasicRangeType, IconSchema } from '../DynamicEngine/schema';
|
||||||
// import styles from './index.less';
|
// import styles from './index.less';
|
||||||
@ -141,6 +142,11 @@ const FormEditor = (props: FormEditorProps) => {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
{item.type === 'Table' && (
|
||||||
|
<Form.Item label={item.name} name={item.key} valuePropName="data">
|
||||||
|
<Table data={item.data} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
26
src/components/Table/index.less
Normal file
26
src/components/Table/index.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
248
src/components/Table/index.tsx
Normal file
248
src/components/Table/index.tsx
Normal 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);
|
||||||
@ -13,7 +13,7 @@ export default memo(function ZanPao() {
|
|||||||
<div className={styles.takeCat}>
|
<div className={styles.takeCat}>
|
||||||
<Popover placement="top" title={null} content={content} trigger="hover">
|
<Popover placement="top" title={null} content={content} trigger="hover">
|
||||||
<Button type="primary" danger>
|
<Button type="primary" danger>
|
||||||
请作者喝茶🍵~
|
赞助作者
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user