mirror of
https://github.com/MrXujiang/h5-Dooring.git
synced 2026-02-03 23:28:12 +00:00
🆕 新增数据可视化组件Chart,搭建可视化数据配置流程
This commit is contained in:
parent
0858944c77
commit
c81e15efaa
@ -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
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';
|
||||
|
||||
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;
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
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}>
|
||||
<Popover placement="top" title={null} content={content} trigger="hover">
|
||||
<Button type="primary" danger>
|
||||
请作者喝茶🍵~
|
||||
赞助作者
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user