本篇文章继续讲一下React工程化项目中,业务组件的常用封装。
省份城市过滤器
我们先定义地区的结构列表:
export const BelongCityCodeList = [
{
code: "11",
name: "北京市",
children: [
{
code: "1101",
name: "北京市",
},
],
},
...
]
外层定义省份,children内定义该省份的所有城市。接着写自己的过滤组件:
...
import { Cascader } from "antd";
import { BelongCityCodeList } from "../../utils/cityList";
function BelongCityFilter(props) {
const { value } = props;
return (
<Cascader
value={value}
fieldNames={{ label: "name", value: "code", children: "children" }}
options={BelongCityCodeList}
placeholder="请选择归属地"
/>
);
}
export default BelongCityFilter;
使用antd的级联选择组件,结构与上述列表一致。
自定义搜索
当然也可以自定义可搜索的列表。通过API获取可选择的列表,然后传给自定义组件来进行过滤筛选。
const [customerOptions, setCustomerOptions] = useState(customerList);
customerList从后台获取,传给本组件的state。然后定义一个下拉选择:
import { Select } from "antd";
const { Option } = Select;
<Select
showSearch
mode={mode} // 默认单选
style={{ width: width || '100%' }}
placeholder="请选择客户"
value={value}
onChange={onChange}
onClick={e => e && e.preventDefault}
dropdownMatchSelectWidth={500}
onSearch={getFilterOption}
showArrow={false}
{...resProps}
>
{customerOptions.map((item, index) => (
<Option key={index} value={item.Id}>
{`${item.Name}-(${item.Id})`}
</Option>
))}
</Select>
然后对于搜索,可自定义筛选规则:
const getFilterOption = (value) => {
const filterList = customerList.filter(item => {
return item.Name?.toLowerCase().indexOf(value.toLowerCase()) >= 0
})
setCustomerOptions(filterList)
}
日期级联组合选择框
先定义格式化的日期按钮:
const dateList = ["当日", "本周", "上月"];
然后定义按钮组:
import { DatePicker, Radio } from 'antd';
<Radio.Group
options={[...dateList.map(item => (
{ label: item, value: item }
)), { label: "自定义", value: "自定义" }]}
onChange={onRadioChange}
defaultValue={rangeBtn}
value={rangeBtn}
optionType="button"
buttonStyle="solid"
size={size}
/>
定义默认选中:
const defaultRangeBtn = "当日";
const [rangeBtn, setRangeBtn] = useState(defaultRangeBtn);
定义选中按钮的事件:
const onRadioChange = e => {
setRangeBtn(e.target.value);
if (e.target.value !== "自定义") {
const dateRange = getDateRange(e.target.value);
setDates(dateRange);
onChange(dateRange, e.target.value);
}
};
通过 getDateRange
获取时间区间,以此输出最后的时间 dates:
import moment from 'moment';
export const getDateRange = (dateKey) => {
return dateRange[dateKey]
}
export const dateRange = {
当日: [moment().startOf("day"), moment().endOf("day")],
上周: [
moment()
.subtract(1, "week")
.startOf("week")
.startOf("day"),
moment()
.subtract(1, "week")
.endOf("week")
.endOf("day")
],
上月: [
moment()
.subtract(1, "month")
.startOf("month")
.startOf("day"),
moment()
.subtract(1, "month")
.endOf("month")
.endOf("day")
],
本月: [
moment()
.startOf("month")
.startOf("day"),
moment().endOf("day")
],
...
}
最后放置自定义时的日期选择框:
<RangePicker
value={dates}
onChange={onDatesChange}
size={size}
disabled={rangeBtn !== "自定义"}
format="YYYY/MM/DD HH:mm:ss"
onOpenChange={(open) => {
// 打开选择面板时清空已选择
if (open) {
setDates(undefined)
}
}}
{...rangePickerOptions}
/>
其中选中事件如下:
const onDatesChange = (values) => {
setDates(values);
onChange(rangeDates, rangeBtn);
}
onChange
为该组件统一对外暴露的事件。按钮组和日期框的级联可以用 Space
组件进行组合。
上传与下载文件
先定义上传按钮:
import { Upload, Button, message } from 'antd';
// 上传按钮,可自定义按钮样式Content
export function UploadButton(props) {
const { Content, buttonProps, RelatedModuleId, RelatedId, backFetch, ...resProps } = props;
const uploadProps = getUploadProps(RelatedModuleId, RelatedId, backFetch);
return <Upload {...uploadProps} {...resProps}>
{
Content ?
<Content /> :
<Button icon={<UploadOutlined />} {...buttonProps}>选择文件</Button>
}
</Upload>
}
其中属性获取 getUploadProps:
// 上传组件基本参数,需自定义可通过组件props设置
const getUploadProps = (RelatedModuleId, RelatedId, backFetch) => ({
name: 'file',
multiple: true, // 上传多个文件
// action: '/apis/upload', // 上传文件API地址
customRequest: (file) => fetchUpload(file, RelatedModuleId, RelatedId, backFetch), // 自定义上传方法,覆盖默认的上传方式
beforeUpload: () => false, // 不上传,用于表单中,最后在form的确定按钮之后自行调API上传
onChange(info) {
const { status } = info.file;
// 自定义的上传后处理
if (status !== 'uploading') {
//console.log(info.file, info.fileList);
}
if (status === 'done') {
message.success(`${info.file.name} file uploaded successfully.`);
} else if (status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
},
});
fetchUpload
为后端API。
再来看看下载链接定义,相对简单一些:
<a
href={ImportTemplateFieldId.template_file}
download
>
下载导入模版
</a>,
关键点是获取URL。我们还是使用资源配置的方式,在资源管理列表,用户可以上传模板文件,上传后会生成一个唯一的文件UUID,在界面加载时会向后端获取,拼接后存在ImportTemplateFieldId
中,这里便可以直接拿到。
折线图
信息平台最常用的图表是折线图和柱状图,这里以折线图为例进行记录。
引入g2Plot:
import { Line } from '@antv/g2plot';
新建一个 Line 画图对象:
const container = useRef(null);
const LineChart = () => {
if (container.current) {
container.current.innerHTML = ""; // 强制重渲染
}
const linePlot = new Line(container.current, {
data,
xField,
yField,
xAxis,
yAxis,
seriesField,
tooltip,
smooth: true,
...resProps
})
linePlot.render();
}
属性字段可通过props获取,这里给一个示例:
<Trend
data={dateStatistics}
xField="SendDate"
yField="Value"
seriesField="Serie"
yAxis={{
label: {
// 千分位数字
formatter: (v) => `${getSthousandsFormatNum(v)} 条`,
},
}}
tooltip={{
formatter: (datum) => {
return { name: datum.Serie, value: `${getSthousandsFormatNum(datum.Value)} 条` };
},
}}
/>
数据的结构可以是这样的:
[
{
SendDate: ’2020-01-01‘,
Value: 258,
Serie: '发送失败'
},
{
SendDate: ’2020-01-01‘,
Value: 1,
Serie: '发送成功'
},
{
SendDate: ’2020-01-01‘,
Value: 259,
Serie: '发送总量'
}
...
]
最后返回一个渲染Line的容器即可:
return <div ref={container} style={{ minHeight: '260px', minWidth: "500px", ...style }} />
数据表格
这里对antd的Table进行二次封装,为了使用更简便。
先对单独的Table进行包裹一下:
import React from "react";
import { Table } from 'antd';
import { getPaginationOption } from '../../utils/getPaginationOption';
/* 带标准分页模式的表格 */
function XSTable(props) {
const { dataSource, columns, loading, position, onPaginationChange, pagination, ...rest } = props;
const PaginationOption = getPaginationOption({ ...position, onChange: onPaginationChange });
const tableStyle = {
bordered: true,
size: "small",
}
const tableProps = {
columns,
dataSource,
loading,
pagination: pagination === undefined ? PaginationOption : pagination
}
return (
<Table
{...tableStyle}
{...rest}
{...tableProps}
/>
)
}
export default XSTable
其中的分页样式提了出来,单独配置:
// 符合antd表格规范的通用表格分页样式
export function getPaginationOption(props) {
const { total, pageSize, current, onChange } = props;
const onPaginationChange = (cur, size) => {
const pagination = {
total,
pageSize: size,
current: cur
}
onChange(pagination)
}
return {
size: "small",
showSizeChanger: true,
showQuickJumper: true,
showTotal: total => `共 ${total} 条`,
pageSize,
current,
total,
onChange: onPaginationChange,
}
}
上述封装后,节省了每个Table都要定义一套分页器、样式的问题,但是对于表格搜索过滤和前后端分页的问题没有解决,所以还要继续改造。
定义一个更高阶的组件:
function XTable(props) {
const {
dataSource, loading, columns, totalCount, hiddenColumns, // table参数
filterFields, defaultFilters, filterRowCount, defaultExpand, // filter参数
handleDownload, downloadLoading, columnsSetting, leftActions, rightActions, // 按钮
pagination, // 分页,不设置的话默认API分页,设置了就是按设置的走,如果想要前端分页,设置pagination={{ showTotal: total => `共 ${total} 条` }}
onChange, ...rest } = props;
return (
<div>
// 这里放搜索表单
<XSTable
{...rest}
dataSource={dataSource}
loading={loading}
columns={tableColumns}
position={position}
pagination={pagination}
onPaginationChange={onPaginationChange}
/>
</div>
)
}
说明一下设定:我们定义一堆 filter 参数用于搜索过滤,同时对于分页操作,我们想要的效果是走默认的分页是后端分页,想要前端分页可以自己传自定义的pagination,在该pagination中不调用API即可。
先定义一下分页变更事件;
const [position, setPosition] = useState({
total: 0, pageSize: 10, current: 1
});
const onPaginationChange = (value) => {
setPosition(value);
getList(filters, value);
}
其中获取API的方法为getList:
const getList = (filters, position) => {
const params = {
Limit: position.pageSize,
Offset: position.current - 1,
...filters,
}
onChange && onChange(params)
}
onChange 为外部传入,在onChange中调用API即可,API会刷新datasource变化,进而影响组件内部的展示,position来记录分页状态。如果想要前端分页,可以这样传入:
pagination={{ showTotal: total => `共 ${total} 条` }}
这样就没有了默认的那个onPaginationChange事件了,antd自动走前端分页。
接下来说一下搜索。
在放置搜索表单的地方定义一个搜索组件:
<TableFilter
onSearch={onSearch}
fields={filterFields}
leftActions={leftActions}
rightActions={tableAllRightActions}
defaultValue={defaultFilters}
rowCount={filterRowCount}
defaultExpand={defaultExpand}
/>
在该组件里返回一个antd的Form:
import { Form, Button, Col, Row } from 'antd';
const [form] = Form.useForm();
return (
<Form
form={form}
name="advanced_search"
style={{ marginBottom: 20 }}
onFinish={onFinish}
>
{fields && fields.length > 0 && (
<Row gutter={24}>{getFields()}</Row>
)}
<Button
{ ...okButtonProps }
type="primary"
size="small"
htmlType="submit"
>
{okText || '搜索'}
</Button>
<Button
style={{ margin: '0 8px' }}
size="small"
onClick={() => {
form.resetFields();
form.setFieldsValue(defaultValue);
}}
>
重置
</Button>
{fields && fields.length > count ? (
<Button
type="link"
size="small"
style={{ padding: '0 4px' }}
onClick={() => {
setExpand(!expand);
}}
>
{expand ? <UpOutlined /> : <DownOutlined />}{' '}
更多
</Button>
) : null}
</Form>
)
可以看到, fields 属性存放表单数据,给一个示例:
const fields = [
{
label: "名称",
key: "Name",
type: "Input",
},
{
label: "年龄",
key: "Age",
type: "NumberInput",
},
]
针对属性的 type,你当然还可以封装一个专门用来展示表单的组件,是Input就显示普通输入框,是NumberInput就显示数字框等。
另外还有重置按钮调用的是form的自带API。更多可以展开多行搜索,还可以收起来,fields区域需要动态展示:
const defaultCount = 4;
const count = rowCount ? rowCount : defaultCount;
const getFields = () => {
// 根据展开状态来决定显示几行
const showFields = expand ? fields : fields.slice(0, count);
const children = [];
showFields.forEach(item => {
if (!item.hidden) {
children.push(
<Col span={item.span ? item.span : 6} key={item.key}>
<Form.Item
name={item.key}
label={item.label}
required={item.required}
>
// 显示表单组件的组件
{FormItemComponent(item)}
</Form.Item>
</Col>
);
}
});
return children;
};
最后点击搜索按钮后,会触发表单的submit事件,自动调用onFinish:
const onFinish = values => {
onSearch(values);
};
在XTable中:
const [filters, setFilters] = useState(defaultFilters);
const onSearch = (value) => {
setFilters(value);
const resetPosition = {
current: 1,
pageSize: position.pageSize,
total: totalCount
};
setPosition(resetPosition);
getList(value, resetPosition);
}
搜索后默认回到第一页,重置分页器,并且带上参数请求API。
表格还可以定义 leftActions、rightActions等扩展属性,用于表格的操作。同时还可以向上边图示那样,写一个Popover
来动态展示当前可显示的列,并且将选择结果存在localStorage中,用于保存用户使用状态。
常见的通用组件就这么多了,有新的会随后补充~~