无邪的楼房 · 张豆豆(中国艺术体操运动员)_百度百科· 4 月前 · |
淡定的葡萄 · Devices and Queues :: ...· 5 月前 · |
虚心的荒野 · 斗罗大陆神界青楼小说小舞和河神最新章节列表_ ...· 6 月前 · |
体贴的南瓜 · Jetpack Compose | ...· 10 月前 · |
礼貌的消防车 · 写生二十年 | ...· 10 月前 · |
一个后台管理系统的组件库,它主要包括以下几个模块:
由于在Formily中已经介绍过输入组件了,这篇文章主要介绍Antd的页面布局组件与展示组件。
ant design的官方文档在 这里 ,ant design Pro Components的官方文档在 这里
代码在 这里
npm create @umijs/umi-app
npm install
使用umi脚手架创建项目
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
//hash路由
history: {
type: 'hash',
//打开locale
locale: { antd: true },
//https://umijs.org/zh-CN/plugins/plugin-antd
//紧凑主题,或者暗黑主题
antd: {
//dark: true,
compact: true,
fastRefresh: {},
修改.umirc.ts配置文件,配置路由,locale,以及antd的插件配置
import { Table, Popconfirm, Button } from 'antd';
import { useState } from 'react';
import { DatePicker, message } from 'antd';
const ProductList = () => {
const [date, setDate] = useState(null);
const handleChange = (value) => {
message.info(
`您选择的日期是: ${
value ? value.format('YYYY年MM月DD日') : '未选择'
setDate(value);
const products = [{ name: 'fish' }, { name: 'cat' }];
const columns = [
title: 'Name',
dataIndex: 'name',
return (
<DatePicker onChange={handleChange} />
<Table dataSource={products} columns={columns} />
export default ProductList;
在basic/index.tsx下面创建测试页面,启动即可
1.2 主题配置
代码在这里
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
//hash路由
history: {
type: 'hash',
//打开locale
locale: { antd: true },
//https://umijs.org/zh-CN/plugins/plugin-antd
//紧凑主题,或者暗黑主题
antd: {
//dark: true,
compact: true,
theme: {
'primary-color': '#1DA57A',
'link-color': '#1DA57A',
'border-radius-base': '2px',
fastRefresh: {},
在.umirc.ts里面配置theme就可以了,轻松设置主题色
2 区块间布局组件,ProCard
代码在这里
ProCard的布局相当方便灵活,基本能解决大部分的布局问题。
基础,title,tooltip,extra和children
import React from 'react';
import ProCard from '@ant-design/pro-card';
export default () => {
return (
<div style={{ background: 'rgb(240, 242, 245)', padding: '20px' }}>
<ProCard
//ProCard默认是flex布局,不是inline-flex布局
title="默认尺寸" //标题
tooltip="这是提示" //标题旁边的问号,表示tooltip
extra="extra" //右上角内容
style={{ maxWidth: 300 }}
<div>Card content</div>
<div>Card content</div>
<div>Card content</div>
</ProCard>
<ProCard
title="小尺寸卡片"
tooltip="这是提示"
extra="extra"
style={{ maxWidth: 300, marginTop: 24 }}
size="small" //小号的尺寸
<div>Card content</div>
<div>Card content</div>
<div>Card content</div>
</ProCard>
基础使用,这点没啥好说的
2.2 操作项,actions
在ProCard的底部可以设置actions,这些actions之间会自动有线区分开来
import React from 'react';
import ProCard from '@ant-design/pro-card';
import {
EditOutlined,
EllipsisOutlined,
SettingOutlined,
} from '@ant-design/icons';
export default () => {
return (
<div style={{ background: 'rgb(240, 242, 245)', padding: '20px' }}>
<ProCard
title="Actions 操作项"
style={{ maxWidth: 300 }}
//actions是设置项的描述,会自带垂直的分割线
actions={[
<SettingOutlined key="setting" />,
<EditOutlined key="edit" />,
<EllipsisOutlined key="ellipsis" />,
<div>Card content</div>
<div>Card content</div>
<div>Card content</div>
</ProCard>
这点也不太难
2.3 内容是标签布局,tabs
ProCard的内容可以是简单的ReactNode,也可以是一个标签页。竖的标签页或者横的标签页都支持
import React, { useState } from 'react';
import ProCard from '@ant-design/pro-card';
export default () => {
const [tab, setTab] = useState('tab2');
return (
<div style={{ background: 'rgb(240, 242, 245)', padding: '20px' }}>
<ProCard
tabs={{
tabPosition: 'top',
<ProCard.TabPane key="tab1" tab="产品一">
</ProCard.TabPane>
<ProCard.TabPane key="tab2" tab="产品二">
</ProCard.TabPane>
</ProCard>
<ProCard
style={{ marginTop: '10px' }}
tabs={{
//可以为card的展示方式
type: 'card',
//可以为左侧显示模式
tabPosition: 'left',
//可以为受控模式
activeKey: tab,
onChange: (key) => {
setTab(key);
<ProCard.TabPane key="tab1" tab="产品一">
</ProCard.TabPane>
<ProCard.TabPane key="tab2" tab="产品二">
</ProCard.TabPane>
</ProCard>
标签页的每个内容写在children里面,用key来区分开。可以用activeKey与onChange实现受控标签,也可以不受控。适合在单个页面中一次写完多个标签内容的页面。注意,内容要嵌套在ProCard.TabPane的组件里面。
2.4 区块间栅栏布局,colSpan
ProCard可以实现栅栏布局的效果
import React from 'react';
import ProCard from '@ant-design/pro-card';
export default () => {
return (
<div style={{ background: 'rgb(240, 242, 245)', padding: '20px' }}>
<ProCard
direction="column" //使用了flex-direction
ghost
gutter={[0, 8]} //使用了flex的gap
<ProCard
layout="center" //justify-content为center,以及align-items也为center,效果就是上下左右都是中间
bordered //四周有边框
colSpan - 24
</ProCard>
<ProCard
colSpan={12} //colSpan设置了宽度的比例,而且设置了flex-shrink为0,不能收缩
layout="center"
bordered
colSpan - 12
</ProCard>
<ProCard colSpan={8} layout="center" bordered>
colSpan - 8
</ProCard>
<ProCard colSpan={0} layout="center" bordered>
colSpan - 0
</ProCard>
</ProCard>
<ProCard gutter={8} title="24栅格" style={{ marginTop: 8 }}>
<ProCard colSpan={12} layout="center" bordered>
colSpan-12
</ProCard>
<ProCard colSpan={6} layout="center" bordered>
colSpan-6
</ProCard>
<ProCard colSpan={6} layout="center" bordered>
colSpan-6
</ProCard>
</ProCard>
<ProCard style={{ marginTop: 8 }} gutter={8} ghost>
<ProCard colSpan="200px" layout="center" bordered>
colSpan - 200px
</ProCard>
<ProCard layout="center" bordered>
</ProCard>
</ProCard>
<ProCard style={{ marginTop: 8 }} gutter={8} ghost>
<ProCard bordered layout="center">
</ProCard>
<ProCard
colSpan="30%" //colSpan可以为单独的比例,而不是数字
bordered
colSpan - 30%
</ProCard>
</ProCard>
只需要在外层套一个ProCard,内层的ProCard设置好colSpan就可以了。内层的ProCard支持colSpan为整数,像素值,甚至是比例。外层的ProCard可以是无title的,也可以设置direction与gutter。
2.5 区块间分栏布局,split
栅栏布局就是指定每个区块的占用的宽度比例,区块之间是留有空间的。但是分栏布局是区块之间没有空隙的布局,仅仅用一条分割线来切分。
import React, { useState } from 'react';
import ProCard from '@ant-design/pro-card';
import RcResizeObserver from 'rc-resize-observer';
export default () => {
return (
<div style={{ background: 'rgb(240, 242, 245)', padding: '20px' }}>
<ProCard
title="左右分栏带标题"
extra="2019年9月28日"
split={'horizontal'} //上下分层,水平的分界线,split的特点是,父子ProCard之间的padding消失了。子ProCard的圆角也消失了
bordered
headerBordered //是指header与content之间的分界线
<ProCard title="左侧详情" colSpan="50%">
<div style={{ height: 100 }}>左侧内容</div>
</ProCard>
<ProCard title="流量占用情况">
<div style={{ height: 100 }}>右侧内容</div>
</ProCard>
</ProCard>
<ProCard split="vertical" style={{ marginTop: '10px' }}>
<ProCard title="左侧详情" colSpan="30%">
</ProCard>
<ProCard title="左右分栏子卡片带标题" headerBordered>
<div style={{ height: 360 }}>右侧内容</div>
</ProCard>
</ProCard>
与colSpan的用法类似,外部的ProCard用split来生成分栏,然后在里面嵌套ProCard就可以了。Split为horizontal就是水平的分割线,上下布局。Split为vertical是垂直的分割线,左右布局。
2.6 区块间是微分割线,Divider
区块间是分栏布局的话,区块间的分割线是一栏到底的,这是因为父级的ProCard与子级ProCard之间的padding消失了。但是有时候,我们只是希望这种分栏的样式不要太彻底,轻微的分割线,不希望padding消失。
import React, { useState } from 'react';
import { Statistic } from 'antd';
import ProCard from '@ant-design/pro-card';
const { Divider } = ProCard;
export default () => {
//使用ProCard.Group的话,分割的宽度更为准确,
//其实与ProCard也区别不大,可以直接用
return (
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
<ProCard.Group title="核心指标" direction={'row'}>
<ProCard>
<Statistic title="今日UV" value={79.0} precision={2} />
</ProCard>
<Divider type={'vertical'} />
<ProCard>
<Statistic
title="冻结金额"
value={112893.0}
precision={2}
</ProCard>
<Divider type={'vertical'} />
<ProCard>
<Statistic title="信息完整度" value={93} suffix="/ 100" />
</ProCard>
<Divider type={'vertical'} />
<ProCard>
<Statistic title="冻结金额" value={112893.0} />
</ProCard>
</ProCard.Group>
<ProCard
title="核心指标"
direction={'row'}
style={{ marginTop: '10px' }}
<ProCard>
<Statistic title="今日UV" value={79.0} precision={2} />
</ProCard>
<Divider type={'vertical'} />
<ProCard>
<Statistic
title="冻结金额"
value={112893.0}
precision={2}
</ProCard>
<Divider type={'vertical'} />
<ProCard>
<Statistic title="信息完整度" value={93} suffix="/ 100" />
</ProCard>
<Divider type={'vertical'} />
<ProCard>
<Statistic title="冻结金额" value={112893.0} />
</ProCard>
</ProCard>
这个时候用ProCard下面嵌套有Divider就可以实现这种微分割线的分栏布局。注意,外层的ProCard是用ProCard.Group还是ProCard,差异不大,可以混着用。注意,Divider是用ProCard的Divider,不是AntDesign的Divider。
单页面布局组件,PageContainer
代码在这里
PageContainer描述的是一个页面常用的展示方式
基础,title,extra,content,extraContent,footer与tabList
一个pageContainer包含的基础元素
import { PageContainer } from '@ant-design/pro-layout';
import { Button } from 'antd';
const Layout: React.FC<any> = (props) => {
//content是页面Header的内容
//tabList是可选的tab列表
//extra是右上角的内容
//footer是底部数据,以fixed的形式存在
return (
<PageContainer
title="我是标题"
content="欢迎使用 ProLayout 组件"
extraContent="我是额外内容"
tabList={[
tab: '基本信息',
key: 'base',
tab: '详细信息',
key: 'info',
extra={[
<Button key="3">操作</Button>,
<Button key="2">操作</Button>,
<Button key="1" type="primary">
</Button>,
footer={[
<Button key="rest">重置</Button>,
<Button key="submit" type="primary">
</Button>,
{props.children}
</PageContainer>
export default Layout;
都是很显然的数据,没啥好说的。注意,PageContainer与ProCard对于标签布局实现的不同,PageContainer的children总是由开发者指定的当前标签页内容,ProCard的children是开发者自己指定所有的标签页的内容。
ProCard的标签适合在一个页面里写完所有标签页内容,PageContainer更适合是放在layout中,不同页面指向到不同的标签页内容。
面包屑与可控tab,breadcrumb,tabActiveKey和onTabChange
PageContainer更为细节的内容,包括subTitle,tags,面包屑breadcrumb,标签样式,以及tabList的受控操作
import React, { useState } from 'react';
import { EllipsisOutlined } from '@ant-design/icons';
import { Button, Dropdown, Menu, Tag } from 'antd';
import { PageContainer } from '@ant-design/pro-layout';
import ProCard from '@ant-design/pro-card';
import { genBreadcrumbProps } from '@ant-design/pro-layout/lib/utils/getBreadcrumbProps';
const Layout: React.FC<any> = (props) => {
const [activeKey, setActiveKey] = useState('base');
return (
style={{
background: '#F5F7FA',
<PageContainer
//放在header位置
header={{
title: '页面标题',
subTitle: '子标题',
tags: <Tag color="blue">Running</Tag>,
//ghost是让背景设置为透明
ghost: true,
//面包屑,重要的展示内容
breadcrumb: {
routes: [
//前端的位置,path点击以后都是可以跳转的
path: '/basic/picker',
breadcrumbName: '一级页面',
path: '/basic/table',
breadcrumbName: '二级页面',
//当前页面的path没有作用
path: '',
breadcrumbName: '当前页面',
//右上角的展示内容
extra: [
<Button key="1">次要按钮</Button>,
<Button key="2">次要按钮</Button>,
<Button key="3" type="primary">
</Button>,
<Dropdown
key="dropdown"
trigger={['click']}
overlay={
//overlay是点击以后的展示内容
<Menu.Item key="1">下拉菜单</Menu.Item>
<Menu.Item key="2">下拉菜单2</Menu.Item>
<Menu.Item key="3">下拉菜单3</Menu.Item>
</Menu>
//DropDown的children是它的展示方式
<Button key="4" style={{ padding: '0 8px' }}>
<EllipsisOutlined />
</Button>
</Dropdown>,
//在header里面有extra的话,外部的extra就不会起作用
extra={[
<Button key="1">次要按钮</Button>,
<Button key="2">次要按钮</Button>,
content="欢迎使用 ProLayout 组件"
tabActiveKey={activeKey}
tabList={[
tab: '基本信息',
key: 'base',
//closeable为false就是没有关闭按钮
closable: false,
tab: '详细信息',
key: 'info',
tabProps={{
//tab的展示样式,这里
//基础样式有line、card editable-card
//https://ant.design/components/tabs-cn/#Tabs
//editable-card,表示标签页可以删除
type: 'editable-card',
//标签页有+号
hideAdd: false,
//新增与删除标签页时候的回调
onEdit: (e, action) => console.log(e, action),
//标签页点击时的回调
onTabChange={(value) => {
//value是标签的key
setActiveKey(value);
console.log('tab change to', value);
footer={[
//底部按钮群,fixed形式,也没啥好说的
<Button key="3">重置</Button>,
<Button key="2" type="primary">
</Button>,
{props.children}
</PageContainer>
export default Layout;
breadCrumb是面包屑,定义在header里面。另外,tabActiveKey与onTabChange就是tabList的受控操作,tabProps是定义标签的样式。
ghost可以让头部的内容透明,部分时候会用到。
4 多页面布局组件,ProLayout
代码在这里
4.1 基础
ProLayout的基础元素,而且layout为mix,splitMenus为true,
layout为side
layout为top
import React, { useState } from 'react';
import { Button, Descriptions, Result, Avatar, Space, Statistic } from 'antd';
import { LikeOutlined, UserOutlined } from '@ant-design/icons';
import type { ProSettings } from '@ant-design/pro-layout';
import ProLayout, {
PageContainer,
SettingDrawer,
DefaultFooter,
} from '@ant-design/pro-layout';
import route from './route';
const content = (
<Descriptions size="small" column={2}>
<Descriptions.Item label="创建人">张三</Descriptions.Item>
<Descriptions.Item label="联系方式">
<a>421421</a>
</Descriptions.Item>
<Descriptions.Item label="创建时间">2017-01-10</Descriptions.Item>
<Descriptions.Item label="更新时间">2017-10-10</Descriptions.Item>
<Descriptions.Item label="备注">
中国浙江省杭州市西湖区古翠路
</Descriptions.Item>
</Descriptions>
export default () => {
const mixModeSetting = {
fixSiderbar: true, //可调的左侧群
navTheme: 'light', //light的主题模式
primaryColor: '#1890ff', //菜单主题色
layout: 'mix', //混合布局,左侧与顶端都是
contentWidth: 'Fluid', //流式内容布局,宽度总是会自动调整
splitMenus: true, //分割菜单,一级菜单在顶部,其他菜单在左侧
fixedHeader: false,
menuHeaderRender: false,
const [settings, setSetting] = useState<Partial<ProSettings> | undefined>({
fixSiderbar: true,
const [pathname, setPathname] = useState('/welcome');
return (
id="test-pro-layout"
style={{
height: '100vh',
<ProLayout
//定义左侧菜单的路由
route={route}
//定义当前页面的location
location={{
pathname,
//内容部分的底面水印
waterMarkProps={{
content: 'Pro Layout',
//顶部标题
title="Remax"
//顶部logo
logo="https://gw.alipayobjects.com/mdn/rms_b5fcc5/afts/img/A*1NHAQYduQiQAAAAAAAAAAABkARQnAQ"
//左侧菜单栏顶部的header
menuHeaderRender={(logo, title) => (
id="customize_menu_header"
onClick={() => {
window.open('https://remaxjs.org/');
{logo}
{title}
//左侧菜单栏底部的footer
menuFooterRender={(props) => {
return (
style={{
lineHeight: '48rpx',
display: 'flex',
height: 48,
color: 'rgba(255, 255, 255, 0.65)',
alignItems: 'center',
href="https://preview.pro.ant.design/dashboard/analysis"
target="_blank"
rel="noreferrer"
alt="pro-logo"
src="https://procomponents.ant.design/favicon.ico"
style={{
width: 16,
height: 16,
margin: '0 16px',
marginRight: 10,
{!props?.collapsed && //根据是否折叠来显示Preview Remax
'Preview Remax'}
//左侧菜单栏的每个菜单项的渲染
menuItemRender={(item, dom) => (
//每个表单项的包装器,可以设置点击时的触发行为
onClick={() => {
setPathname(item.path || '/welcome');
{dom}
//右侧内容的展示
rightContentRender={() => (
<Avatar
shape="square"
size="small"
icon={<UserOutlined />}
//内容的页脚
footerRender={() => (
<DefaultFooter
links={[
key: 'test',
title: 'layout',
href: 'www.alipay.com',
key: 'test2',
title: 'layout2',
href: 'www.alipay.com',
copyright="这是一条测试文案"
//是否有菜单的可选收缩按钮
{...mixModeSetting}
//可选项
//{...settings}
<PageContainer
//ProLayout会自动计算BreadCump和title,传递给PageContainer
tabList={[
tab: '基本信息',
key: 'base',
tab: '详细信息',
key: 'info',
//PageContainer内容页的信息
content={content}
//PageContainer内容页的右上角
extraContent={
<Space size={24}>
<Statistic
title="Feedback"
value={1128}
prefix={<LikeOutlined />}
<Statistic
title="Unmerged"
value={93}
suffix="/ 100"
</Space>
//header的顶部内容
extra={[
<Button key="3">操作</Button>,
<Button key="2">操作</Button>,
<Button key="1" type="primary">
</Button>,
style={{
height: '120vh',
<Result
status="404"
style={{
height: '100%',
background: '#fff',
title="Hello World"
subTitle="Sorry, you are not authorized to access this page."
extra={<Button type="primary">Back Home</Button>}
</PageContainer>
</ProLayout>
<SettingDrawer
//浮层,用来动态调整Menu的属性
//在实际环境不需要用
pathname={pathname}
getContainer={() => document.getElementById('test-pro-layout')}
settings={settings}
onSettingChange={(changeSetting) => {
setSetting(changeSetting);
disableUrlParams
代码也是比较显然的,注意点如下:
ProLayout会设定children里面的PageContainer元素,会告诉PageContainer的title与breadcrumb的信息,因此我们不需要设定PageContainer也会有标题和面包屑的信息
ProLayout的route是菜单的所有项信息,location的pathname是当前的菜单选中项,menuItemRender就是点击菜单时的触发操作,我们在这里可以做一个当前匹配项的受控操作。
import React from 'react';
import {
SmileOutlined,
CrownOutlined,
TabletOutlined,
AntDesignOutlined,
} from '@ant-design/icons';
export default {
path: '/',
//子级的路由
routes: [
path: '/welcome', //定义path
name: '欢迎', //定义标题
icon: <SmileOutlined />, //定义图标
//component: './Welcome', //定义组件,UMI会识别到这里
path: '/admin',
name: '管理页',
icon: <CrownOutlined />,
access: 'canAdmin', //访问权限,不知道有啥用
//component: './Admin',
//子级的路由
routes: [
path: '/admin/sub-page1',
name: '一级页面',
icon: <CrownOutlined />,
//component: './Welcome',
path: '/admin/sub-page2',
name: '二级页面',
icon: <CrownOutlined />,
//component: './Welcome',
path: '/admin/sub-page3',
name: '三级页面',
icon: <CrownOutlined />,
//component: './Welcome',
name: '列表页',
icon: <TabletOutlined />,
path: '/list',
//component: './ListTableList',
routes: [
path: '/list/sub-page',
name: '一级列表页面',
icon: <CrownOutlined />,
routes: [
path: 'sub-sub-page1',
name: '一一级列表页面',
icon: <CrownOutlined />,
//component: './Welcome',
path: 'sub-sub-page2',
name: '一二级列表页面',
icon: <CrownOutlined />,
//component: './Welcome',
path: 'sub-sub-page3',
name: '一三级列表页面',
icon: <CrownOutlined />,
//component: './Welcome',
path: '/list/sub-page2',
name: '二级列表页面',
icon: <CrownOutlined />,
//component: './Welcome',
path: '/list/sub-page3',
name: '三级列表页面',
icon: <CrownOutlined />,
//component: './Welcome',
path: 'https://ant.design', //可以直接指向外链
name: 'Ant Design 官网外链',
icon: <AntDesignOutlined />,
路由的数据设计:
path,全路径的url
name,页面标题,菜单项名称,和pageContainer里面的title
icon,标题
routes,子级路由
component,暂时不需要用,因为ProLayout只是一个UI组件,不是一个Router组件。
4.2 服务器拉取菜单
我们设置当点击刷新按钮的时候,重新向服务器拉取菜单信息
import React, { useRef } from 'react';
import ProLayout, { PageContainer } from '@ant-design/pro-layout';
import { Button } from 'antd';
import customMenuDate from './route';
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
export default () => {
const actionRef = useRef<{
//触发重新加载菜单
reload: () => void;
}>();
return (
<ProLayout
style={{
height: '100vh',
border: '1px solid #ddd',
//获取menu的action
actionRef={actionRef}
menu={{
request: async () => {
//菜单是异步拉取,再这里拉取
await waitTime(2000);
return customMenuDate;
location={{
//当前的location
pathname: '/welcome/welcome',
<PageContainer
//ProLayout自动计算面包屑,和标题
content="欢迎使用"
Hello World
<Button
style={{
margin: 8,
onClick={() => {
actionRef.current?.reload();
</Button>
</PageContainer>
</ProLayout>
方法也简单,用menu而不是routes,来设置一个获取菜单的闭包函数。然后用actionRef来获取触发重载菜单的选项。在首次加载页面的时候,会自动加载menu。
4.3 服务器菜单的图标选项
服务器加载的菜单也可以含有图标选项的,但是要提前先将所有可能用到的图标都先加载过来。
import React from 'react';
import ProLayout, { PageContainer } from '@ant-design/pro-layout';
import route, { loopMenuItem } from './route';
export default () => (
<ProLayout
style={{
height: 500,
fixSiderbar
location={{
pathname: '/welcome/welcome',
menu={{ request: async () => loopMenuItem(route) }}
<PageContainer content="欢迎使用">
style={{
height: '120vh',
Hello World
</PageContainer>
</ProLayout>
menu拉取菜单的时候,要对菜单进行loopMenuItem的转换操作
import { SmileOutlined, HeartOutlined } from '@ant-design/icons';
import type { MenuDataItem } from '@ant-design/pro-layout';
const IconMap = {
smile: <SmileOutlined />,
heart: <HeartOutlined />,
export default [
path: '/',
name: 'welcome',
icon: 'smile', //以字符串来标记icon
children: [
path: '/welcome',
name: 'one',
icon: 'smile',
children: [
path: '/welcome/welcome',
name: 'two',
icon: 'smile',
exact: true,
path: '/demo',
name: 'demo',
icon: 'heart',
export const loopMenuItem = (menus: MenuDataItem[]): MenuDataItem[] =>
//做icon转换,从字符串到实际的icon
menus.map(({ icon, children, ...item }) => ({
...item,
icon: icon && IconMap[icon as string],
children: children && loopMenuItem(children),
就是这样做,服务器端返回的icon是一个字符串,而不是一个ReactNode。然后在客户端做转换操作,将icon从字符串转换到ReactNode。
5 ProLayout与UMI整合
ProLayout的特别之处,在于它只是一个UI组件,当切换页面的时候,内容自身不会发生变化。我们需要它跟路由组件整合一起,当路由变化的时候,菜单选中项要跟着变化。当菜单选中项点击的时候,要触发路由的变化。
在这里,我们使用UMI作为路由组件。
5.1 UMI声明式路由
代码在这里
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
//hash路由
history: {
type: 'hash',
//layout插件只能支持声明方式路由,不支持约定方式路由
layout: {
// 支持任何不需要 dom 的
// https://procomponents.ant.design/components/layout#prolayout
name: 'Remax',
locale: true,
layout: 'side',
//打开locale
locale: { antd: true },
routes: [
{ name: '首页', path: '/', component: '@/pages/index' },
{ name: 'umi', path: '/umi', component: '@/pages/umi/index' },
//https://umijs.org/zh-CN/plugins/plugin-antd
//紧凑主题,或者暗黑主题
antd: {
//dark: true,
compact: true,
fastRefresh: {},
Umi对ProLayout有很好的默认支持,我们可以在不需要编写任何ProLayout的情况下使用它。我们只需要在.umirc.ts的配置文件中,配置好layout,以及声明式的路由routes就可以了。这个时候,我们就需要显式地写入每个菜单对应的component,以及每个菜单项的icon和name了。
这是效果,这种方法的缺点就是,页面这么多,全部都要重重复复地写声明式路由,真的好麻烦。能不能不写routes,让UMI使用自己的约定式路由来自动配置ProLayout,答案是不行(找不到官方的issue地址了)。
5.2 UMI约定式路由,和面包屑
代码在这里
为了让UMI的约定式路由,与ProLayout协同工作,我们继续试试。
import ProLayout, { DefaultFooter } from '@ant-design/pro-layout';
import route from './route';
import { useHistory, useLocation } from 'umi';
import { useState } from 'react';
export default (props) => {
const history = useHistory();
const location = useLocation();
const mixModeSetting = {
fixSiderbar: true, //可调的左侧群
navTheme: 'light', //light的主题模式
primaryColor: '#1890ff', //菜单主题色
layout: 'mix', //混合布局,左侧与顶端都是
contentWidth: 'Fluid', //流式内容布局,宽度总是会自动调整
splitMenus: true, //分割菜单,一级菜单在顶部,其他菜单在左侧
fixedHeader: false,
menuHeaderRender: false, //不显示左侧的菜单栏logo
return (
id="test-pro-layout"
style={{
height: '100vh',
<ProLayout
//定义左侧菜单的路由
route={route}
//使用location来active对应的menu
location={{
pathname: location.pathname,
//内容部分的底面水印
waterMarkProps={{
content: 'Pro Layout',
//顶部标题
title="Remax"
//顶部logo
logo="https://gw.alipayobjects.com/mdn/rms_b5fcc5/afts/img/A*1NHAQYduQiQAAAAAAAAAAABkARQnAQ"
//左侧菜单栏底部的footer
menuFooterRender={(props) => {
return (
style={{
lineHeight: '48rpx',
display: 'flex',
height: 48,
color: 'rgba(255, 255, 255, 0.65)',
alignItems: 'center',
href="https://preview.pro.ant.design/dashboard/analysis"
target="_blank"
rel="noreferrer"
alt="pro-logo"
src="https://gw.alipayobjects.com/mdn/rms_b5fcc5/afts/img/A*1NHAQYduQiQAAAAAAAAAAABkARQnAQ"
style={{
width: 16,
height: 16,
margin: '0 16px',
marginRight: 10,
{!props?.collapsed && //根据是否折叠来显示Preview Remax
'Preview Remax'}
//左侧菜单栏的每个菜单项的渲染
menuItemRender={(item, dom) => (
//每个表单项的包装器,可以设置点击时的触发行为
onClick={() => {
history.push(item.path || '/welcome');
{dom}
//设置breadCrumb
breadcrumbRender={(route) => {
console.log(route);
return route?.map((single) => {
return {
...single,
path: '#' + single.path,
//内容的页脚
footerRender={() => (
<DefaultFooter
links={[
key: 'test',
title: 'layout',
href: 'www.alipay.com',
key: 'test2',
title: 'layout2',
href: 'www.alipay.com',
copyright="这是一条测试文案"
//是否有菜单的可选收缩按钮
{...mixModeSetting}
{props.children}
</ProLayout>
首先,我们src/layouts/index.tsx中填写以上的文件,这是全局所有页面的约定layout。可以看到,我们将location.pathname作为当前菜单选中项,并且在menuItemRender修改了一下,当点中菜单选中项的时候,触发路由的push操作。这样就实现了,路由与菜单数据同步与联动的目的。而且,当菜单切换的时候,路由发生变化,UMI生成不同的props.children传递给了ProLayout,这样就实现了,当菜单项点击的时候,菜单的选中项(pathname)会发生变化,同时页面的内容也会发生变化。
import React from 'react';
import {
SmileOutlined,
CrownOutlined,
TabletOutlined,
AntDesignOutlined,
} from '@ant-design/icons';
export default {
path: '/',
//子级的路由
//ProLayout使用前缀匹配的原则来匹配哪个菜单
routes: [
extact: true,
//不要定义为/umi路径,因为/umi/admin既匹配/umi/,也匹配/umi/admin,就会造成两个菜单项都点亮了
path: '/welcome', //定义path
name: '欢迎', //定义标题
icon: <SmileOutlined />, //定义图标
path: '/admin',
name: '管理页',
icon: <CrownOutlined />,
routes: [
path: '/admin/sub-page1',
name: '一级页面',
icon: <CrownOutlined />,
path: '/admin/sub-page2',
name: '二级页面',
icon: <CrownOutlined />,
path: '/admin/sub-page3',
name: '三级页面',
icon: <CrownOutlined />,
path: '/user',
name: '用户管理页',
icon: <SmileOutlined />,
//把底层的隐藏掉
//hideChildrenInMenu:true,
routes: [
//为什么该路由不在菜单也要添加到路由中
//因为要满足面包屑的要求,只能在这里添加
path: '/user/add',
name: '添加用户',
hideInMenu: true,
icon: <CrownOutlined />,
path: '/user/view/:userId',
name: '编辑用户',
hideInMenu: true,
icon: <CrownOutlined />,
传入菜单的route数据,我们采用之传入path,name和icon的方式,不再需要传递component了。这是要比声明式路由写省点代码的地方,而且声明式路由要写一大堆的layout。
注意一下,即使添加用户页面,不在菜单项中,也需要写到ProLayout的routes里面。ProLayout需要这些页面的name与嵌套信息来生成PageContainer的标题与面包屑。这明显带来了另外一个问题,路由的项与在UMI配置文件写入路由项的数量是一样多的,即使少了些component与layout的写法。关键原因在于,要补充面包屑与标题信息。
在管理页,我们看到,不同的Tab触发的时候,页面的url会发生变化,而且菜单项也会发生变化,怎么做到的。
import { PageContainer } from '@ant-design/pro-layout';
import { Button, Descriptions, Result, Avatar, Space, Statistic } from 'antd';
import { LikeOutlined, UserOutlined } from '@ant-design/icons';
import { genBreadcrumbProps } from '@ant-design/pro-layout/lib/utils/getBreadcrumbProps';
import { useHistory, useLocation } from 'umi';
const content = (
<Descriptions size="small" column={2}>
<Descriptions.Item label="创建人">张三</Descriptions.Item>
<Descriptions.Item label="联系方式">
<a>421421</a>
</Descriptions.Item>
<Descriptions.Item label="创建时间">2017-01-10</Descriptions.Item>
<Descriptions.Item label="更新时间">2017-10-10</Descriptions.Item>
<Descriptions.Item label="备注">
中国浙江省杭州市西湖区古翠路
</Descriptions.Item>
</Descriptions>
const AdminLayout: React.FC<any> = (props) => {
const location = useLocation();
const history = useHistory();
//ProLayout会自动计算BreadCump和title,传递给PageContainer
return (
<PageContainer
//使用PathName作为tab的activeKey
tabActiveKey={location.pathname}
//定义每个子页面对应的key
tabList={[
tab: '子页面1',
key: '/admin/sub-page1',
tab: '子页面2',
key: '/admin/sub-page2',
tab: '子页面3',
key: '/admin/sub-page3',
//标签页切换的时候,使用history切换页面
onTabChange={(value) => {
history.replace(value);
//PageContainer内容页的信息
content={content}
//PageContainer内容页的右上角
extraContent={
<Space size={24}>
<Statistic
title="Feedback"
value={1128}
prefix={<LikeOutlined />}
<Statistic title="Unmerged" value={93} suffix="/ 100" />
</Space>
//header的顶部内容
extra={[
<Button key="3">操作</Button>,
<Button key="2">操作</Button>,
<Button key="1" type="primary">
</Button>,
{props.children}
</PageContainer>
export default AdminLayout;
方法也是和ProLayout一样,将PageContainer的Tab受控,用location.pathname(填入tabActiveKey),和history.replace(填入onTabChange)中就可以了。注意一下,当前tabList的key要用url来描述。
5.3 UMI约定式路由,和无面包屑
代码在这里
我们的目标是要省事,因为要满足pageContainer的面包屑和标题的信息,我们依然需要往router里面填写所有页面的路由信息,我们进一步简化这一步,不要面包屑了。
import { PageContainer, PageContainerProps } from '@ant-design/pro-layout';
import { useHistory, useLocation } from 'umi';
import { RedoOutlined } from '@ant-design/icons';
import { createContext } from 'react';
import { useContext } from 'react';
type PageAction = {
refresh: () => void;
const PageActionContext = createContext<PageAction>({} as PageAction);
type MyPageContainerProps = PageContainerProps & {
hiddenBack?: boolean;
const MyPageContainer: React.FC<MyPageContainerProps> = (props) => {
const history = useHistory();
const context = useContext(PageActionContext);
return (
<PageContainer
{...props}
onBack={
props.hiddenBack
? undefined
: () => {
history.goBack();
extra={
props.extra ? (
props.extra
) : (
<RedoOutlined
style={{ fontSize: '20px' }}
onClick={() => {
context.refresh();
{props.children}
</PageContainer>
export default MyPageContainer;
export { PageActionContext };
首先定义一个MyPageContainer,有onBack的返回按钮。
import MyPageContainer from '@/components/MyPageContainer';
import { Link, useLocation, useRouteMatch } from 'umi';
export default () => {
return (
<MyPageContainer title={'单位管理页面'} hiddenBack={true}>
<h1>{'列表页面'}</h1>
</MyPageContainer>
然后每个页面用MyPageContainer包裹起来,填写自己的标题信息,可以选择有或者无,返回按钮。
import React from 'react';
import {
SmileOutlined,
CrownOutlined,
TabletOutlined,
AntDesignOutlined,
} from '@ant-design/icons';
export default {
path: '/',
//子级的路由
//ProLayout使用前缀匹配的原则来匹配哪个菜单
routes: [
extact: true,
path: '/welcome', //定义path
name: '欢迎', //定义标题
icon: <SmileOutlined />, //定义图标
path: '/item',
name: '商品管理',
icon: <CrownOutlined />,
routes: [
path: '/item/unit',
name: '单位管理',
icon: <CrownOutlined />,
path: '/item/item',
name: '商品管理',
icon: <CrownOutlined />,
path: '/user',
name: '用户管理',
icon: <SmileOutlined />,
routes: [
path: '/user/admin',
name: '用户管理',
icon: <CrownOutlined />,
path: '/user/privilege',
name: '权限管理',
icon: <CrownOutlined />,
最后,我们在路由中只填写菜单项的信息就可以了,不需要填写所有页面的路由信息。
这样详情页也不需要在routes中显式写入,但它依然有标题信息,只是少了面包屑而已。省事!
6 按钮,Button
代码在这里
按钮的元素包括:
文本children,children内部可以自由安排其他ReactNode,例如是其他icon。
图标icon,总是放在children的左边。
类型type,包括primary,dashed,text与link。
形状shape,包括undefined(方形),round(圆角)与circle(圆形)
加载中loading,就是一个loading图标,并且loading的时候不能触发点击
下拉Dropdown,外层包围组件,有Dropdown,与Dropdown.Button两种。有trigger,与overlay两个选项。
import { Button, Dropdown, Menu, Space } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined, EllipsisOutlined } from '@ant-design/icons';
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="基础" bordered headerBordered>
<Space size={10}>
<Button type="primary">Primary Button</Button>
<Button>Default Button</Button>
<Button type="dashed">Dashed Button</Button>
<Button type="text">Text Button</Button>
<Button type="link">Link Button</Button>
</Space>
</ProCard>
<ProCard title="图标与形状" bordered headerBordered>
<Space size={10}>
<Button
type="primary"
icon={<SearchOutlined />} //带图标的按钮
Search
</Button>
<Button
type="primary" //图标放在内容里面
Search
<SearchOutlined />
</Button>
<Button
type="primary"
shape="round"
icon={<SearchOutlined />} //带图标的按钮
Search
</Button>
<Button
icon={<SearchOutlined />} //带图标的按钮
<Button
shape="circle"
icon={<SearchOutlined />} //带图标的按钮
<Button
shape="round"
icon={<SearchOutlined />} //带图标的按钮
</Space>
</ProCard>
<ProCard title="加载中" bordered headerBordered>
<Space size={10}>
<Button
type="primary"
loading={true}
icon={<SearchOutlined />} //带图标的按钮
Search
</Button>
</Space>
</ProCard>
<ProCard title="下拉多按钮" bordered headerBordered>
<Space size={10}>
<Dropdown.Button
//Dropdown.Button就会产生两部分,Button以及Dropdown的图标部分
//Button部分,Actions本身不会产生下拉列表
//Dropdown的图标部分,overlay的才会产生下拉列表
overlay={
<Menu.Item key="1">1st item</Menu.Item>
<Menu.Item key="2">2nd item</Menu.Item>
<Menu.Item key="3">3rd item</Menu.Item>
</Menu>
Actions
</Dropdown.Button>
<Dropdown
//DropDown包围下的整个组件都会产生下拉列表的
//可以设置trigger为click
trigger={['click']}
overlay={
<Menu.Item key="1">1st item</Menu.Item>
<Menu.Item key="2">2nd item</Menu.Item>
<Menu.Item key="3">3rd item</Menu.Item>
</Menu>
<Button>
Actions
<EllipsisOutlined />
</Button>
</Dropdown>
<Dropdown
//右键产生的上下文菜单
trigger={['contextMenu']}
overlay={
<Menu.Item key="1">1st item</Menu.Item>
<Menu.Item key="2">2nd item</Menu.Item>
<Menu.Item key="3">3rd item</Menu.Item>
</Menu>
<Button>
Actions
<EllipsisOutlined />
</Button>
</Dropdown>
</Space>
</ProCard>
</Space>
内容也是没啥好说的,都很直观。注意Dropdown与Dropdown.Button的区别
7 徽标,Badge
代码在这里
徽标的主要用法有三种:
children,包裹一个任意组件,在右上角展示徽标值
count,仅展示一个数字
status与text,徽标仅作为小圆点,辅助文本展示
import { Button, Dropdown, Menu, Space, Avatar, Badge } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined, UserOutlined } from '@ant-design/icons';
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="包裹组件的徽标" bordered headerBordered>
<Space size={20}>
<Badge count={10}>
<Avatar
size={64}
shape="square"
icon={<UserOutlined />}
</Badge>
<Badge count={0}>
<Avatar
size={64}
shape="square"
icon={<UserOutlined />}
</Badge>
<Badge
count={0}
showZero //默认0值不显示
<Avatar
size={64}
shape="square"
icon={<UserOutlined />}
</Badge>
<Badge
count={100}
overflowCount={99} //封顶数默认为99
<Avatar
size={64}
shape="square"
icon={<UserOutlined />}
</Badge>
<Badge dot>
<Avatar
size={64}
shape="square"
icon={<UserOutlined />}
</Badge>
</Space>
</ProCard>
<ProCard title="不包裹组件的徽标" bordered headerBordered>
<Space size={10}>
<Badge count={10} />
<Badge count={0} showZero />
<Badge count={100} />
<Badge dot />
</Space>
</ProCard>
<ProCard title="展示文本组件的徽标" bordered headerBordered>
<Space size={10}>
<Badge status="success" text="Success" />
<Badge status="error" text="Error" />
<Badge status="default" text="Default" />
<Badge status="processing" text="Processing" />
<Badge status="warning" text="Warning" />
</Space>
</ProCard>
</Space>
使用还是很直观的,注意点如下:
Badge包围一个普通组件的时候,徽标默认会在右上角的位置展示
当使用count数值的时候,我们可选项有showZero,overflowCount与dot,用来辅助count数值如何展示
当传入的属性有status与text属性的时候,count属性不起作用。
8 标记,Tag
代码在这里
Tag组件的元素包括:
children,Tag展示的内容
closable与onClose,是否有关闭按钮,以及关闭按钮的触发
color,颜色
icon,图标
import { Button, Dropdown, Menu, Space, Tag } from 'antd';
import ProCard from '@ant-design/pro-card';
import {
SearchOutlined,
CheckCircleOutlined,
SyncOutlined,
} from '@ant-design/icons';
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="基础" bordered headerBordered>
<Space size={10}>
<Tag>Tag 1</Tag>
closable
onClose={(e) => {
e.preventDefault();
console.log('close');
Tag 2
</Space>
</ProCard>
<ProCard title="color" bordered headerBordered>
<Space size={10}>
<Tag color="magenta">magenta</Tag>
<Tag color="red">red</Tag>
<Tag color="volcano">volcano</Tag>
<Tag color="orange">orange</Tag>
<Tag color="gold">gold</Tag>
<Tag color="lime">lime</Tag>
<Tag color="green">green</Tag>
<Tag color="cyan">cyan</Tag>
<Tag color="blue">blue</Tag>
<Tag color="geekblue">geekblue</Tag>
<Tag color="purple">purple</Tag>
</Space>
</ProCard>
<ProCard title="icon" bordered headerBordered>
<Space size={10}>
<Tag icon={<CheckCircleOutlined />} color="success">
success
<Tag icon={<SyncOutlined spin />} color="processing">
processing
</Space>
</ProCard>
</Space>
使用依然很直观,注意一下Tag与Badge在展示纯文本的样式不同。Tag是整个底色都不同的,Badge仅仅是小圆点的颜色不同而已。
9 卡片,Card
代码在这里
ProCard是主要的布局组件,以及带有轻量功能的展示组件。那么Card组件是重量级的展示组件,它的展示功能比ProCard更强,但是它的布局功能较弱,基本不推荐用来布局。
卡片组件的元素包括:
title,标题
extra,右侧内容
cover,封面
actions,触发按钮
children,指定用Card.Meta。Card.Meta可以区分Card建立自己的属性,包括title,标题,description,描述,和avatar,头像
import { Button, Dropdown, Menu, Space, Tag, Card, Avatar } from 'antd';
import ProCard from '@ant-design/pro-card';
import {
EditOutlined,
EllipsisOutlined,
SettingOutlined,
} from '@ant-design/icons';
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="基础" bordered headerBordered>
<Space size={10}>
title="我是标题"
extra="我是extra"
style={{ width: 300 }}
cover={
alt="example"
src="https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png"
actions={[
<SettingOutlined key="setting" />,
<EditOutlined key="edit" />,
<EllipsisOutlined key="ellipsis" />,
<Card.Meta
avatar={
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
title="Card title"
description="This is the description"
</Card>
</Space>
</ProCard>
</Space>
这个还是很简单的,注意一下Card.Meta总是作为children嵌套在Card组件里面就可以了
10 统计展示组件,Statistic
代码在这里
AntDesign提供了两套统计的展示组件,分别是普通版本和Pro版本,我们基本上只需要使用Pro版本就可以了,普通版本实在太弱了。
统计数值的展示组件的设计很考验对业务的理解,我觉得Pro版本的接口就设计得很好。
10.1 基础组件,Statistic
AntDesign的普通版本的Statistic组件,元素包括:
title,标题
value,值
precision,精度,保留几位小数
prefix,value的前缀
suffix,value的后缀
import { Button, Dropdown, Menu, Space, Statistic } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined, LikeOutlined } from '@ant-design/icons';
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="title,value与precision" bordered headerBordered>
<Space size={20}>
<Statistic title="Active Users" value={112893} />
<Statistic
title="Account Balance (CNY)"
value={112893}
precision={2}
</Space>
</ProCard>
<ProCard title="prefix与suffix" bordered headerBordered>
<Space size={20}>
<Statistic
title="Feedback"
value={1128}
prefix={<LikeOutlined />}
<Statistic title="Unmerged" value={93} suffix="/ 100" />
</Space>
</ProCard>
</Space>
注意,图标与/100的数字都是通过prefix与suffix来添加的。功能也比较简单,就不啰嗦了。
10.2 统计组件,Pro-Statistic
ProStatistic相比普通Statistic有更强大实用的功能,它的元素有:
title,value,precision,prefix,suffix,这些普通版本都有介绍
layout,可以垂直放置
trend,放在value左边的向上,向下图标,实用
status,放在title的左边的红绿圆点,实用
icon,放在title与value的坐标,实用
description,可以嵌套存放其他的Statistic,这个设计很棒!主要不是放在children属性里面。
import { Button, Dropdown, Menu, Space } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined, LikeOutlined } from '@ant-design/icons';
import { StatisticCard } from '@ant-design/pro-card';
//注意这个是来自于pro-card的,不是antd的
const { Statistic } = StatisticCard;
const imgStyle = {
display: 'block',
width: 42,
height: 42,
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="指标默认是横放的" bordered headerBordered>
<Space
//value可以是number,也可以是string
//字符串的话,精度就不起作用了
size={20}
<Statistic
title="实际完成度"
value={82.3}
prefix={<LikeOutlined />}
<Statistic title="实际完成度" value={'82'} suffix="/ 100" />
<Statistic title="当前目标" value={6000} precision={2} />
<Statistic title="当前目标" value={'¥6000'} precision={2} />
</Space>
</ProCard>
<ProCard title="竖放" bordered headerBordered>
<Space size={20}>
<Statistic
value={15.1}
title="累计注册数"
suffix="万"
layout="vertical"
<Statistic
value={15.1}
title="本月注册数"
suffix="万"
layout="vertical"
</Space>
</ProCard>
<ProCard title="趋势" bordered headerBordered>
<Space size={20}>
<Statistic
layout="vertical"
title="日同比"
value="6.15%"
trend="up"
<Statistic
layout="vertical"
title="日同比"
value="3.85%"
trend="down"
</Space>
</ProCard>
<ProCard title="状态" bordered headerBordered>
<Space size={20}>
<Statistic
layout="vertical"
title="日同比"
value="6.15%"
status="success"
<Statistic
layout="vertical"
title="日同比"
value="-3.85%"
status="error"
</Space>
</ProCard>
<ProCard title="图标" bordered headerBordered>
<Space size={20}>
<Statistic
layout="vertical"
title={'支付金额'}
value={2176}
icon={
style={imgStyle}
src="https://gw.alipayobjects.com/mdn/rms_7bc6d8/afts/img/A*dr_0RKvVzVwAAAAAAAAAAABkARQnAQ"
alt="icon"
<Statistic
layout="vertical"
title={'访客数'}
value={475}
icon={
style={imgStyle}
src="https://gw.alipayobjects.com/mdn/rms_7bc6d8/afts/img/A*-jVKQJgA1UgAAAAAAAAAAABkARQnAQ"
alt="icon"
</Space>
</ProCard>
<ProCard title="描述" bordered headerBordered>
<Space size={20}>
<Statistic
layout="vertical"
title={'支付金额'}
value={2176}
description={
<Space direction="vertical">
<Statistic title="实际完成度" value="82.3%" />
<Statistic title="当前目标" value="¥6000" />
</Space>
</Space>
</ProCard>
</Space>
代码还是简单的。注意,当使用icon的时候,默认layout就是vertical的,不需要显式传入也可以。
统计卡片组件,Pro-StatisticCard
Statistic只是展示一个值而已,真实的统计组件,还需要完整的相关图表,所以有了StatisticCard组件,它的元素有:
title,卡片标题
extra,卡片右上侧内容
tip,卡片标题提示
statistic,卡片的数值展示
chart,图表,默认为卡片数值的下方
更进一步,StatisticCard组件的元素有:
footer,底部的其他statistic信息,有自动的横线分隔开
children可以嵌套其他的statistic,分主次地展示多个统计数值
chartPlacement=left,左侧显示图表
import { Button, Dropdown, Menu, Space } from 'antd';
import ProCard from '@ant-design/pro-card';
import {
SearchOutlined,
LikeOutlined,
EllipsisOutlined,
} from '@ant-design/icons';
import { StatisticCard } from '@ant-design/pro-card';
//注意这个是来自于pro-card的,不是antd的
const { Statistic } = StatisticCard;
const imgStyle = {
display: 'block',
width: 42,
height: 42,
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard
title="title,extra,statistic与chart"
bordered
headerBordered
<Space size={20}>
<StatisticCard
title={'部门'}
//右上角内容
extra={<EllipsisOutlined />}
//主体统计信息
statistic={{
value: 1102893,
prefix: '¥',
//主体的图表
chart={
src="https://gw.alipayobjects.com/zos/alicdn/BA_R9SIAV/charts.svg"
alt="chart"
width="100%"
style={{ width: 268 }}
</Space>
</ProCard>
<ProCard title="tip,无statistic" bordered headerBordered>
<Space size={20}>
<StatisticCard
title="大盘趋势"
//标题的提示信息
tip="大盘说明"
style={{ maxWidth: 480 }}
extra={<EllipsisOutlined />}
chart={
src="https://gw.alipayobjects.com/zos/alicdn/a-LN9RTYq/zhuzhuangtu.svg"
alt="柱状图"
width="100%"
</Space>
</ProCard>
<ProCard title="footer" bordered headerBordered>
<Space size={20}>
<StatisticCard
title="整体流量评分"
extra={<EllipsisOutlined />}
statistic={{
value: 86.2,
suffix: '分',
chart={
src="https://gw.alipayobjects.com/zos/alicdn/PmKfn4qvD/mubiaowancheng-lan.svg"
width="100%"
alt="进度条"
//图表下面的footer
footer={
<Statistic
value={15.1}
title="累计注册数"
suffix="万"
layout="horizontal"
<Statistic
value={15.1}
title="本月注册数"
suffix="万"
layout="horizontal"
style={{ width: 250 }}
</Space>
</ProCard>
<ProCard title="children嵌套StatisticCard" bordered headerBordered>
<Space size={20}>
<StatisticCard
title={'财年总收入'}
statistic={{
value: 601987768,
description: (
<Statistic
title="日同比"
value="6.15%"
trend="up"
chart={
src="https://gw.alipayobjects.com/zos/alicdn/zevpN7Nv_/xiaozhexiantu.svg"
alt="折线图"
width="100%"
<Statistic
//这个就是图表与footer之间嵌套的内容
title="大盘总收入"
value={1982312}
layout="vertical"
description={
<Statistic
title="日同比"
value="6.15%"
trend="down"
</StatisticCard>
</Space>
</ProCard>
<ProCard title="chartPlacement" bordered headerBordered>
<Space size={20}>
<StatisticCard
statistic={{
title: '付费流量',
value: 3701928,
description: (
<Statistic title="占比" value="61.5%" />
chart={
src="https://gw.alipayobjects.com/zos/alicdn/ShNDpDTik/huan.svg"
alt="百分比"
width="100%"
//将图表放在统计信息的左边
chartPlacement="left"
</Space>
</ProCard>
</Space>
代码还是简单的,注意footer与children嵌套Statistic的用法,这个还是挺好用的。
统计卡片分组组件,Pro-StatisticGroup
当我们有了多个统计卡片的时候,我们需要使用像ProCard这样的布局组件来在一个页面中排列这些卡片,于是有了StatisticGroup的布局组件,只为StatisticCard而来。
其实StatisticCard.Group就是ProCard,将代码从StatisticCard.Group换成ProCard也不会有错,它们都是一样的布局组件。
import { Button, Dropdown, Menu, Space } from 'antd';
import ProCard from '@ant-design/pro-card';
import {
SearchOutlined,
LikeOutlined,
EllipsisOutlined,
} from '@ant-design/icons';
import { StatisticCard } from '@ant-design/pro-card';
//注意这个是来自于pro-card的,不是antd的
const { Statistic, Operation, Divider } = StatisticCard;
const imgStyle = {
display: 'block',
width: 42,
height: 42,
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<StatisticCard.Group
//StatisticCard.Group 就是ProCard
direction={'row'}
<StatisticCard
statistic={{
title: '总流量(人次)',
value: 601986875,
<Divider type={'vertical'} />
<StatisticCard
statistic={{
title: '付费流量',
value: 3701928,
description: <Statistic title="占比" value="61.5%" />,
chart={
src="https://gw.alipayobjects.com/zos/alicdn/ShNDpDTik/huan.svg"
alt="百分比"
width="100%"
chartPlacement="left"
<Divider type={'vertical'} />
<StatisticCard
statistic={{
title: '免费流量',
value: 1806062,
description: <Statistic title="占比" value="38.5%" />,
chart={
src="https://gw.alipayobjects.com/zos/alicdn/6YR18tCxJ/huanlv.svg"
alt="百分比"
width="100%"
chartPlacement="left"
</StatisticCard.Group>
<StatisticCard.Group
//标题信息
title="核心指标"
direction={'row'}
<StatisticCard
statistic={{
title: '今日UV',
tip: '供应商信息',
value: 79,
precision: 2,
<Divider type={'vertical'} />
<StatisticCard
statistic={{
title: '冻结金额',
value: 112893,
precision: 2,
suffix: '元',
<Divider type={'vertical'} />
<StatisticCard
statistic={{
title: '信息完整度',
value: 92,
suffix: '/ 100',
<Divider type={'vertical'} />
<StatisticCard
statistic={{
title: '冻结金额',
value: 112893,
precision: 2,
suffix: '元',
</StatisticCard.Group>
<StatisticCard.Group>
<StatisticCard
statistic={{
title: '服务网格数',
value: 500,
<Operation
//关键是这个的用法,替换Divider
</Operation>
<StatisticCard
statistic={{
title: '未发布',
value: 234,
<Operation>+</Operation>
<StatisticCard
statistic={{
title: '发布中',
value: 112,
<Operation>+</Operation>
<StatisticCard
statistic={{
title: '已发布',
value: 255,
</StatisticCard.Group>
</Space>
代码没啥好说的,和ProCard类似。就是要注意一下,Divider要用StatsitcCard或者ProCard的Divider,而不是AntDesign的Divider,否则分割线展示出不来。
11 树形展示组件,TreeSelect
代码在这里
树形组件,也是很简单的,只是一个title与children的元素而已
import React, { useState } from 'react';
import { Tree, Switch } from 'antd';
import { CarryOutOutlined, FormOutlined } from '@ant-design/icons';
const treeData = [
title: 'parent 1',
key: '0-0',
children: [
title: 'parent 1-0',
key: '0-0-0',
children: [
title: 'leaf',
key: '0-0-0-0',
title: (
//title可以是一个ReactNode
<div>multiple line title</div>
<div>multiple line title</div>
key: '0-0-0-1',
title: 'leaf',
key: '0-0-0-2',
title: 'parent 1-1',
key: '0-0-1',
children: [
title: 'leaf',
key: '0-0-1-0',
title: 'parent 1-2',
key: '0-0-2',
children: [
title: 'leaf',
key: '0-0-2-0',
title: 'leaf',
key: '0-0-2-1',
title: 'parent 2',
key: '0-1',
children: [
title: 'parent 2-0',
key: '0-1-0',
children: [
title: 'leaf',
key: '0-1-0-0',
title: 'leaf',
key: '0-1-0-1',
const Demo: React.FC<{}> = () => {
const onSelect = (selectedKeys: React.Key[], info: any) => {
console.log('selected', selectedKeys, info);
return (
showLine={true}
showIcon={false}
defaultExpandedKeys={['0-0', '0-1']}
//defaultExpandParent={true}
onSelect={onSelect}
//传入是一个数组
treeData={treeData}
export default Demo;
代码也简单
12 列表展示组件,List
代码在这里
List是一个列表的展示组件。List组件与TreeSelect组件最大的不同是,TreeSelect要展示的内容基本是固定的,就是一个图标与标题。但是List组件是允许每行开发者渲染任意的内容,所以,List组件除了dataSource的属性,还会有renderItem的属性。
12.1 基础列表
功能一目了然,元素有:
Header,头部
Footer,尾部
条目,List.Item,对于每个条目的渲染。
import { Space, List } from 'antd';
import ProCard from '@ant-design/pro-card';
const data = [
'Racing car sprays burning fuel into crowd.',
'Japanese princess to wed commoner.',
'Australian walks 100km after outback crash.',
'Man charged over missing wedding girl.',
'Los Angeles battles huge wildfires.',
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="basic" bordered headerBordered>
header={<div>Header</div>}
footer={<div>Footer</div>}
bordered
dataSource={data}
renderItem={(item) => (
<List.Item
//总是用List.Item来包住
{item}
</List.Item>
</ProCard>
</Space>
代码也简单,注意,renderItem的返回值总是用List.Item包围住的。
12.2 完整功能列表
完整功能的List的元素包括:
List.Item.Meta,这里的设计就像Card.Meta一样,这里有Avatar,头像,Title,标题,Description,描述。
List.Item的actions,右侧的按钮
List.Item的extra,右侧内容
List.Item的children,任意嵌套的内容
import { Space, List, Avatar } from 'antd';
import ProCard from '@ant-design/pro-card';
const data = [
title: 'Ant Design Title 1',
title: 'Ant Design Title 2',
title: 'Ant Design Title 3',
title: 'Ant Design Title 4',
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="basic" bordered headerBordered>
header={<div>Header</div>}
footer={<div>Footer</div>}
bordered
dataSource={data}
renderItem={(item) => (
<List.Item
//右侧的actions
actions={[
<a key="list-loadmore-edit">edit</a>,
<a key="list-loadmore-more">more</a>,
//右侧的extra
extra={
width={272}
alt="logo"
src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
<List.Item.Meta
avatar={
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
title={
<a href="https://ant.design">
{item.title}
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
//children的内容
content
</List.Item>
</ProCard>
</Space>
代码也直观
12.3 垂直布局列表
垂直布局,仅仅是更换了actions与children的位置而已,其他不变。分页组件的使用我们会在Table组件的时候更详细地描述
import { Space, List, Avatar } from 'antd';
import ProCard from '@ant-design/pro-card';
const data = [
title: 'Ant Design Title 1',
title: 'Ant Design Title 2',
title: 'Ant Design Title 3',
title: 'Ant Design Title 4',
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="basic" bordered headerBordered>
header={<div>Header</div>}
footer={<div>Footer</div>}
//vertical的layout以后
//children与content都会放在decription的底部
itemLayout="vertical"
bordered
//分页的信息
pagination={{
onChange: (page) => {
console.log(page);
pageSize: 3,
dataSource={data}
renderItem={(item) => (
<List.Item
//右侧的actions
actions={[
<a key="list-loadmore-edit">edit</a>,
<a key="list-loadmore-more">more</a>,
//右侧的extra
extra={
width={272}
alt="logo"
src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
<List.Item.Meta
avatar={
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
title={
<a href="https://ant.design">
{item.title}
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
//children的内容
content
</List.Item>
</ProCard>
</Space>
代码也简单
13 表格组件,Table
代码在这里。表格可谓是组件库中最为复杂的组件了,它既有展示功能,也有输入功能,而且需求还特别多,在这里,仅展示最为常用的功能,更多的要看官方文档。
13.1 基础,dataSource与columns
一个表格的简单使用,要列,以及像List一样的对每个单元格的渲染render方法。
import { Button, Dropdown, Menu, Space, Tag, Table } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined } from '@ant-design/icons';
const columns = [
title: '名字', //标题
dataIndex: 'name', //dataIndex
key: 'name', //key,一般与dataIndex一致的
render: (text: string) => <a>{text}</a>, //渲染每个单元格的数据,第1个参数为单元格,第2个参数为行数据,第3个参数是index
title: '年龄',
dataIndex: 'age',
key: 'age',
title: '地址',
dataIndex: 'address',
key: 'address',
title: '标记',
key: 'tags',
dataIndex: 'tags',
render: (tags: string[]) => (
{tags.map((tag) => {
let color = tag.length > 5 ? 'geekblue' : 'green';
if (tag === 'loser') {
color = 'volcano';
return (
<Tag color={color} key={tag}>
{tag.toUpperCase()}
title: 'Action',
key: 'action',
render: (text: string, record: any, index: number) => (
<Space size="middle">
Invite {record.name},{index + 1}
<a>Delete</a>
</Space>
const data = [
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser'],
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher'],
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="基础" bordered headerBordered>
<Table
//默认就有分页控件的
columns={columns}
dataSource={data}
</ProCard>
</Space>
columns描述列是怎样的,而且该列的每一个单元格的怎么渲染。注意,columns里面的render方法的每个参数是什么。另外一个传递参数就是dataSource,数据源自身。这样的设计将数据,与渲染本身切分开来了。
另外,要注意,对于dataSource的每一行在渲染的时候都需要一个key属性,以满足React对数组渲染的要求。或者,你可以在Table组件中,指定rowKey属性是什么,这个属性可以是一个字符串,也可以是一个方法,用来计算每一行的key是什么。
头部,尾部与边框,header,footer与bordered
表格的头部,尾部与边框
import { Button, Dropdown, Menu, Space, Tag, Table } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined } from '@ant-design/icons';
const columns = [
title: '名字', //标题
dataIndex: 'name', //dataIndex
key: 'name', //key,一般与dataIndex一致的
render: (text: string) => <a>{text}</a>, //渲染每个单元格的数据,第1个参数为单元格,第2个参数为行数据,第3个参数是index
title: '年龄',
dataIndex: 'age',
key: 'age',
title: '地址',
dataIndex: 'address',
key: 'address',
title: '标记',
key: 'tags',
dataIndex: 'tags',
render: (tags: string[]) => (
{tags.map((tag) => {
let color = tag.length > 5 ? 'geekblue' : 'green';
if (tag === 'loser') {
color = 'volcano';
return (
<Tag color={color} key={tag}>
{tag.toUpperCase()}
title: 'Action',
key: 'action',
render: (text: string, record: any, index: number) => (
<Space size="middle">
Invite {record.name},{index + 1}
<a>Delete</a>
</Space>
const data = [
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser'],
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher'],
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="header,footer与bordered" bordered headerBordered>
<Table
columns={columns}
dataSource={data}
bordered
title={() => 'Header'}
footer={() => 'Footer'}
</ProCard>
</Space>
这个也是没啥好说的了
13.3 选择行,rowSelection
对每一行的选择,可以是单选,也可以是复选
import { Button, Dropdown, Menu, Space, Tag, Table } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined } from '@ant-design/icons';
import React, { useState } from 'react';
const columns = [
title: 'Name',
dataIndex: 'name',
title: 'Age',
dataIndex: 'age',
title: 'Address',
dataIndex: 'address',
const data = [
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
key: '4',
name: 'Disabled User',
age: 99,
address: 'Sidney No. 1 Lake Park',
export default () => {
let [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>();
const rowSelection = {
//可以设置为单选
//type: 'radio',
selectedRowKeys: selectedRowKeys,
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
//第1个参数是选中的key,第2个参数是选中的行
console.log(
`selectedRowKeys: ${selectedRowKeys}`,
'selectedRows: ',
selectedRows,
setSelectedRowKeys(selectedRowKeys);
getCheckboxProps: (record: any) => ({
//可以设定哪个行,不能被选中
disabled: record.name.indexOf('Joe') != -1,
name: record.name,
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="选择行" bordered headerBordered>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={data}
</ProCard>
</Space>
在Table组件中传入rowSelection属性就可以了,rowSelection属性的selectedRowKeys与onChange构成了选择行的受控操作
树型数据的展开操作,dataSource的children
对于树形数据,每一行都是共用一个列的,Table组件默认就支持了。只需要在dataSource里面用children属性来描述下一个子行就可以了。
import { Button, Dropdown, Menu, Space, Tag, Table } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined } from '@ant-design/icons';
const columns = [
title: 'Name',
dataIndex: 'name',
key: 'name',
title: 'Age',
dataIndex: 'age',
key: 'age',
width: '12%',
title: 'Address',
dataIndex: 'address',
width: '30%',
key: 'address',
const data = [
key: 1,
name: 'John Brown sr.',
age: 60,
address: 'New York No. 1 Lake Park',
children: [
key: 11,
name: 'John Brown',
age: 42,
address: 'New York No. 2 Lake Park',
key: 12,
name: 'John Brown jr.',
age: 30,
address: 'New York No. 3 Lake Park',
children: [
key: 121,
name: 'Jimmy Brown',
age: 16,
address: 'New York No. 3 Lake Park',
key: 13,
name: 'Jim Green sr.',
age: 72,
address: 'London No. 1 Lake Park',
children: [
key: 131,
name: 'Jim Green',
age: 42,
address: 'London No. 2 Lake Park',
children: [
key: 1311,
name: 'Jim Green jr.',
age: 25,
address: 'London No. 3 Lake Park',
key: 1312,
name: 'Jimmy Green sr.',
age: 18,
address: 'London No. 4 Lake Park',
key: 2,
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="tree与children" bordered headerBordered>
<Table
columns={columns}
dataSource={data}
//默认遇到children字段就会出现伸展按钮,以展示树形数据,该字段不设置也会自动生效的
//childrenColumnName={'children'}
</ProCard>
</Space>
这点也是简单的。
特定数据的展开操作,expandable
对于特定数据的展开操作,每一行的列,与展开数据的列是不同的,相当于每行嵌套了一个新的表格
import ProCard from '@ant-design/pro-card';
import { Table, Badge, Menu, Dropdown, Space } from 'antd';
import { DownOutlined } from '@ant-design/icons';
const menu = (
<Menu.Item>Action 1</Menu.Item>
<Menu.Item>Action 2</Menu.Item>
</Menu>
function NestedTable() {
const expandedRowRender = (record: any, index: number) => {
console.log('expandableRender record:', record, ':index', index);
const columns = [
{ title: 'Date', dataIndex: 'date', key: 'date' },
{ title: 'Name', dataIndex: 'name', key: 'name' },
title: 'Status',
key: 'state',
render: () => (
<Badge status="success" />
Finished
</span>
title: 'Upgrade Status',
dataIndex: 'upgradeNum',
key: 'upgradeNum',
title: 'Action',
dataIndex: 'operation',
key: 'operation',
render: () => (
<Space size="middle">
<a>Pause</a>
<a>Stop</a>
<Dropdown overlay={menu}>
More <DownOutlined />
</Dropdown>
</Space>
const data = [];
for (let i = 0; i < 3; ++i) {
data.push({
key: i,
date: '2014-12-24 23:12:00',
name: 'This is production name',
upgradeNum: 'Upgraded: 56',
return <Table columns={columns} dataSource={data} pagination={false} />;
const columns = [
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Platform', dataIndex: 'platform', key: 'platform' },
{ title: 'Version', dataIndex: 'version', key: 'version' },
{ title: 'Upgraded', dataIndex: 'upgradeNum', key: 'upgradeNum' },
{ title: 'Creator', dataIndex: 'creator', key: 'creator' },
{ title: 'Date', dataIndex: 'createdAt', key: 'createdAt' },
{ title: 'Action', key: 'operation', render: () => <a>Publish</a> },
const data = [];
for (let i = 0; i < 3; ++i) {
data.push({
key: i,
name: 'Screem',
platform: 'iOS',
version: '10.3.4.5654',
upgradeNum: 500,
creator: 'Jack',
createdAt: '2014-12-24 23:12:00',
return (
<Table
className="components-table-demo-nested"
columns={columns}
expandable={{ expandedRowRender }}
dataSource={data}
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="可弹出方式的子表格" bordered headerBordered>
<NestedTable />
</ProCard>
</Space>
Table组件的expandable属性,可以传入expandedRowRender,这样就能自定义每一行嵌套的ReactNode了,可以是一个新的Table,也可以是普通的展示组件。
13.6 无分页,pagination为false
无分页,就是在一个Table中全部显示所有行数据,不进行分页
import { Button, Dropdown, Menu, Space, Tag, Table } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined } from '@ant-design/icons';
const columns = [
title: '名字', //标题
dataIndex: 'name', //dataIndex
key: 'name', //key,一般与dataIndex一致的
render: (text: string) => <a>{text}</a>, //渲染每个单元格的数据,第1个参数为单元格,第2个参数为行数据,第3个参数是index
title: '年龄',
dataIndex: 'age',
key: 'age',
title: '地址',
dataIndex: 'address',
key: 'address',
const data = [];
for (var i = 0; i != 100; i++) {
data.push({
key: i,
name: 'fish_' + i,
age: i,
address: 'address_' + i,
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="无分页" bordered headerBordered>
<Table
columns={columns}
dataSource={data}
//不显式分页器,无论数据有多少,都在一页里面显式完毕
pagination={false}
</ProCard>
</Space>
在Table组件中传入pagination为false就可以了,这个方法比较少用
13.7 有分页,pagination受控
分页可以看成是一个受控组件,我们有区分前端分页,或者后端分页的做法。
import { Button, Dropdown, Menu, Space, Tag, Table } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined } from '@ant-design/icons';
import { useState } from 'react';
const columns = [
title: '名字', //标题
dataIndex: 'name', //dataIndex
key: 'name', //key,一般与dataIndex一致的
render: (text: string) => <a>{text}</a>, //渲染每个单元格的数据,第1个参数为单元格,第2个参数为行数据,第3个参数是index
title: '年龄',
dataIndex: 'age',
key: 'age',
title: '地址',
dataIndex: 'address',
key: 'address',
const data = [];
for (var i = 0; i != 100; i++) {
data.push({
key: i,
name: 'fish_' + i,
age: i,
address: 'address_' + i,
export default () => {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setCurrentPageSize] = useState(10);
const setCurrentPageCallback = (current: number) => {
setTimeout(() => {
setCurrentPage(current);
}, 100);
const onShowSizeChange = (current: number, size: number) => {
setTimeout(() => {
setCurrentPage(current);
setCurrentPageSize(size);
}, 100);
console.log(currentPage, pageSize);
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="分页" bordered headerBordered>
<Table
columns={columns}
//只传入前10条数据,后端分页
//dataSource={data.slice(0, 10)}
//前端分页,就是传入整个data就可以了
dataSource={data}
//不显式分页器,无论数据有多少,都在一页里面显式完毕
pagination={{
current: currentPage, //当前页是哪个页,从1开始计数
onChange: setCurrentPageCallback, //当前页的用户触发更改
total: data.length, //传入的data总数
showTotal: (total, range) => `共${total}条`, //显式有多少条总数
showQuickJumper: true, //快速跳页
showSizeChanger: true,
pageSize: pageSize,
onShowSizeChange: onShowSizeChange,
</ProCard>
</Space>
Table组件区分前端还是后端分页的方法很简单,就是当dataSource可以获取偏移到指定页的内容时,就拿这个数据(前端分页)。否则,就从数据的头部展示对应数量的数据(后端分页)。pagination的受控有两个属性:
current,当前页,数值从1开始。current与onChange组合成受控操作
pageSize,页的数据大小。pageSize与onShowSizeChange组合成受控操作
total值不是受控操作,它只能从开发者传入的,不能由用户修改的。total值决定了一共有多少页,以及在分页位置显示的总数信息。
13.8 列嵌套,column的children
列信息嵌套,这个就比较少用了
import { Button, Dropdown, Menu, Space, Tag, Table } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined } from '@ant-design/icons';
const columns = [
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 100,
fixed: 'left',
title: 'Other',
//使用Children就可以nestedHeader
children: [
title: 'Age',
//子节点需要有dataIndex,key与width
dataIndex: 'age',
key: 'age',
width: 150,
//非子节点,只需要一个title
title: 'Address',
children: [
title: 'Street',
dataIndex: 'street',
key: 'street',
width: 150,
title: 'Block',
children: [
title: 'Building',
dataIndex: 'building',
key: 'building',
width: 100,
title: 'Door No.',
dataIndex: 'number',
key: 'number',
width: 100,
title: 'Company',
children: [
title: 'Company Address',
dataIndex: 'companyAddress',
key: 'companyAddress',
width: 200,
title: 'Company Name',
dataIndex: 'companyName',
key: 'companyName',
title: 'Gender',
dataIndex: 'gender',
key: 'gender',
width: 80,
fixed: 'right',
const data = [];
for (let i = 0; i < 100; i++) {
data.push({
key: i,
name: 'John Brown',
age: i + 1,
street: 'Lake Park',
building: 'C',
number: 2035,
companyAddress: 'Lake Street 42',
companyName: 'SoftLake Co',
gender: 'M',
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="嵌套header" bordered headerBordered>
<Table
columns={columns}
dataSource={data}
bordered
scroll={{ x: 'calc(700px + 50%)', y: 240 }}
</ProCard>
</Space>
在列的信息columns上加入一个children属性就可以
合并列与合并行,colSpan与rowSpan
合并行与合并列就更少用了,从图中可以看到,有三个合并操作:
列头合并,Home Phone列横跨两列
行数据的行合并,第3和第4行竖跨2行
行数据的列合并,第5行横跨5列
import { Button, Dropdown, Menu, Space, Tag, Table } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined } from '@ant-design/icons';
// In the fifth row, other columns are merged into first column
// by setting it's colSpan to be 0
const renderContent = (value, row, index) => {
const obj = {
children: value,
props: {},
//第5行,该列不显示,因为被第1列合并了,所以设置colSpan为0
if (index === 4) {
obj.props.colSpan = 0;
return obj;
const columns = [
title: 'Name',
dataIndex: 'name',
render: (text: string, row: any, index: number) => {
//前4行
if (index < 4) {
return <a>{text}</a>;
//第5行
//render不仅可以返回ReactNode,还可以返回object
//children是ReactNode,props是单元格属性,代表合并5列
return {
children: <a>{text}</a>,
props: {
colSpan: 5,
title: 'Age',
dataIndex: 'age',
render: renderContent,
title: 'Home phone',
//列头合并两列
colSpan: 2,
dataIndex: 'tel',
render: (value: string, row: any, index: number) => {
const obj = {
children: value,
props: {},
//第3行,跨2个行
if (index === 2) {
obj.props.rowSpan = 2;
//第4行,不显示行,被第3行合并了,所以设置rowSpan为0
if (index === 3) {
obj.props.rowSpan = 0;
//第5行,该列不显示,因为被第1列合并了,所以设置colSpan为0
if (index === 4) {
obj.props.colSpan = 0;
return obj;
title: 'Phone',
//因为前一列合并了,所以这里要设置colSpan为0,取消显示列头
colSpan: 0,
dataIndex: 'phone',
render: renderContent,
title: 'Address',
dataIndex: 'address',
render: renderContent,
const data = [
key: '1',
name: 'John Brown',
age: 32,
tel: '0571-22098909',
phone: 18889898989,
address: 'New York No. 1 Lake Park',
key: '2',
name: 'Jim Green',
tel: '0571-22098333',
phone: 18889898888,
age: 42,
address: 'London No. 1 Lake Park',
key: '3',
name: 'Joe Black',
age: 32,
tel: '0575-22098909',
phone: 18900010002,
address: 'Sidney No. 1 Lake Park',
key: '4',
name: 'Jim Red',
age: 18,
tel: '0575-22098909',
phone: 18900010002,
address: 'London No. 2 Lake Park',
key: '5',
name: 'Jake White',
age: 18,
tel: '0575-22098909',
phone: 18900010002,
address: 'Dublin No. 2 Lake Park',
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="合并行与列" bordered headerBordered>
<Table columns={columns} dataSource={data} />
</ProCard>
</Space>
注意点如下:
列头合并,就是在columns上面指定colSpan是多少,注意下一列的也要指定colSpan为0,才能正常展示。
行数据的行合并,在columns的render上面,返回一个object,对应的rowSpan为多少,注意下一行的返回的rowSpan要为0,才能正常显示。
行数据的列合并,在columns的render上面,返回一个object,对应的colSpan为多少,注意下一列的返回的colSpan要为0,才能正常显示。
在合并行与合并列的操作中,Table组件的抽象并不完善,开发者要做的操作很多。当然,问题本来就是如此复杂。
13.10
固定列与固定行,fixed与scroll.y
在上下滚动数据的过程中,一部分列保持不动,称为固定列。列头保持不动,称为固定行。
import { Button, Dropdown, Menu, Space, Tag, Table } from 'antd';
import ProCard from '@ant-design/pro-card';
import { SearchOutlined } from '@ant-design/icons';
//每个column都有一个宽度
//当总宽度大于默认值100%的时候,就会出现部分列压缩在一起显示
//最终导致,压缩列的内容竖起来显示,导致行高突然变高了很多,试试把x: 1500打开就看到了
const columns = [
title: 'Full Name',
width: 100,
dataIndex: 'name',
key: 'name',
fixed: 'left', //在左边固定不动,fixedColumn
title: 'Age',
width: 100,
dataIndex: 'age',
key: 'age',
fixed: 'left', //在左边固定不动,fixedColumn
title: 'Column 1',
dataIndex: 'address', //多列之间可以用同一个dataIndex,但不能用同一个key
key: '1',
width: 150,
title: 'Column 2',
dataIndex: 'address',
key: '2',
width: 150,
//这一行的内容超级长,不用ellipsis会导致行高过分增高,因此要加上ellipsis
ellipsis: true,
render: (value) => {
return value + value + value + value;
title: 'Column 3',
dataIndex: 'address',
key: '3',
width: 150,
title: 'Column 4',
dataIndex: 'address',
key: '4',
width: 150,
title: 'Column 5',
dataIndex: 'address',
key: '5',
width: 150,
title: 'Column 6',
dataIndex: 'address',
key: '6',
width: 150,
title: 'Column 7',
dataIndex: 'address',
key: '7', //没有设置宽度,总宽度的剩余宽度会被这个列占用
{ title: 'Column 8', dataIndex: 'address', key: '8', width: 150 },
title: 'Action',
key: 'operation',
fixed: 'right', //在右边固定不动,fixedColumn
width: 100,
render: () => <a>action</a>,
const data = [];
for (let i = 0; i < 100; i++) {
data.push({
key: i,
name: `Edrward ${i}`,
age: 32,
address: `London Park no. ${i}`,
export default () => {
return (
<Space
style={{
background: 'rgb(240, 242, 245)',
padding: '20px',
display: 'flex',
direction="vertical"
size={20}
<ProCard title="合并行与列" bordered headerBordered>
<Table
columns={columns}
dataSource={data}
//scroll的y值仅仅是表格中数据的高度,不包括行头与pageaction
scroll={{ x: 1500, y: 300 }}
</ProCard>
</Space>
设置scroll的y值就能固定列头的行,在columns中指定fixed为left或者right,就能固定列
14 表单,Form
代码在这里
14.1 官方实现
import {
Button,
Dropdown,
Menu,
Space,
Form,
Input,
Checkbox,
} from 'antd';
import ProCard from '@ant-design/pro-card';
import {
SearchOutlined,
CheckCircleOutlined,
SyncOutlined,
} from '@ant-design/icons';
import { useState, useRef } from 'react';
type FormType = {
name: string;
nameId: number;
export default () => {
const [state, setState] = useState(0);
const formData = useRef<FormType>({
name: '',
nameId: 1,
const currentFormData = formData.current;
return (
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
autoComplete="off"
<Form.Item
label="Username"
name="username"
rules={[
{ required: true, message: 'Please input your username!' },
<Input
//在Form下面的,不能使用defaultValue
key={currentFormData.nameId}
defaultValue={currentFormData.name}
onChange={(e) => {
console.log('onChange');
currentFormData.name = e.target.value;
console.log(currentFormData);
</Form.Item>
<Button
onClick={() => {
currentFormData.nameId++;
currentFormData.name = 'jj';
setState((v) => v + 1);
{'设置'}
</Button>
<Form.Item
label="Password"
name="password"
rules={[
{ required: true, message: 'Please input your password!' },
<Input.Password />
</Form.Item>
<Form.Item
name="remember"
valuePropName="checked"
wrapperCol={{ offset: 8, span: 16 }}
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
官方的表单,将展示组件,和数据维护都组合在一起实现了,缺点就是有点太重了,不容易迁移到普通的组件上。
14.2 纯展示组件
import { Button, Input, Checkbox } from 'antd';
import { useState, useRef } from 'react';
import { FormLayout, FormItem } from '@formily/antd';
type FormType = {
name: string;
nameId: number;
nameFeedback: string;
export default () => {
const [state, setState] = useState(0);
const formData = useRef<FormType>({
name: '',
nameId: 1,
nameFeedback: '',
const currentFormData = formData.current;
console.log(currentFormData.nameId);
return (
<FormLayout>
<FormItem
key={'1'}
label="Username"
asterisk={true}
feedbackStatus={
currentFormData.nameFeedback != '' ? 'error' : undefined
feedbackText={currentFormData.nameFeedback}
<Input
//在Form下面的,不能使用defaultValue
key={currentFormData.nameId}
defaultValue={currentFormData.name}
onChange={(e) => {
currentFormData.name = e.target.value;
let newFeedback = '';
!currentFormData.name ||
currentFormData.name == ''
newFeedback = '请输入';
if (newFeedback != currentFormData.nameFeedback) {
currentFormData.nameFeedback = newFeedback;
setState((v) => v + 1);
console.log(currentFormData);
</FormItem>
<Button
onClick={() => {
currentFormData.nameId++;
currentFormData.name = 'jj';
currentFormData.nameFeedback = '';
setState((v) => v + 1);
{'设置'}
</Button>
<FormItem label="Password">
<Input.Password />
</FormItem>
<FormItem>
<Checkbox>Remember me</Checkbox>
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit">
Submit
</Button>
</FormItem>
</FormLayout>
我们可以用Formily的组件,配合纯展示组件FormLayout和FormItem来实现自己的表单模型。要点如下:
默认情况下,表单项的变化不会重新触发render,但会触发更新本地数据。所以,我们要用Input的defaultValue,而不是value,避免每次onChange都需要render。
当需要表单项通过代码来指定修改的时候,修改key键,再重新render就可以了。
缺点就是代码有点繁琐了
14.3 表单逻辑聚合组件
let globalId = 10001;
const getIdName = (key: string | number) => {
return '_' + key + '_id';
const getFeedbackName = (key: string | number) => {
return '_' + key + '_feedback';
type ValidateResult = {
shouldRefresh: boolean;
const BuiltInValidator = {
required: (text: any): string => {
if (text == undefined || text == '' || text == null) {
return '请输入';
} else {
return '';
number: (text: any): string => {
if (/^\d+$/.test(text)) {
return '';
} else {
return '请输入整数';
type ValidatorFunctionType = (text: any) => string;
type ValidatorType = keyof typeof BuiltInValidator | ValidatorFunctionType;
const FormHelper = {
getId<T, K extends keyof T>(target: T, key: K): number {
const current = target as any;
const keyName = getIdName(key as string);
let idValue = current[keyName];
if (idValue == undefined) {
idValue = globalId++;
current[keyName] = idValue;
return idValue;
refreshId<T, K extends keyof T>(target: T, key: K): number {
const current = target as any;
const keyName = getIdName(key as string);
const idValue = globalId++;
current[keyName] = idValue;
return idValue;
getFeedbackStatus<T, K extends keyof T>(
target: T,
key: K,
): 'error' | undefined {
const current = target as any;
const keyName = getFeedbackName(key as string);
let feedbackValue = current[keyName];
if (feedbackValue == undefined || feedbackValue == '') {
return undefined;
} else {
return 'error';
getFeedbackText<T, K extends keyof T>(target: T, key: K): string {
const current = target as any;
const keyName = getFeedbackName(key as string);
let feedbackValue = current[keyName];
if (feedbackValue == undefined || feedbackValue == '') {
return '';
} else {
return feedbackValue as string;
clearValidate<T, K extends keyof T>(target: T, key: K): ValidateResult {
//获取旧feedBack
let oldFeedBack = this.getFeedbackText(target, key);
//赋值新feedback
let current = target as any;
const keyName = getFeedbackName(key as string);
current[keyName] = undefined;
if (oldFeedBack == '') {
return { shouldRefresh: false };
} else {
return { shouldRefresh: true };
validate<T, K extends keyof T>(
target: T,
key: K,
...validator: ValidatorType[]
): ValidateResult {
//获取旧feedBack
let oldFeedBack = this.getFeedbackText(target, key);
//计算newFeedBack
let current = target as any;
let currentValue = current[key];
const validatorResult: string[] = [];
for (let i in validator) {
const singleResult = validator[i];
let singleValidator: ValidatorFunctionType;
if (typeof singleResult == 'function') {
singleValidator = singleResult;
} else {
singleValidator = BuiltInValidator[singleResult];
let fieldResult = singleValidator(currentValue);
if (fieldResult != '') {
validatorResult.push(fieldResult);
let newFeedBack = validatorResult.join(',');
//赋值newFeedBack
const keyName = getFeedbackName(key as string);
current[keyName] = newFeedBack;
//返回是否该刷新
if (oldFeedBack != newFeedBack) {
return {
shouldRefresh: true,
} else {
return {
shouldRefresh: false,
isFormValid<T>(target: T): boolean {
for (let key in target) {
let current = target[key];
let validResult: boolean;
//校验当前节点
if (typeof current == 'function') {
continue;
} else if (typeof current == 'object') {
validResult = this.isFormValid(current);
} else {
const feedbackText = this.getFeedbackText(target, key);
if (feedbackText == '') {
validResult = true;
} else {
validResult = false;
//提前结束
if (validResult == false) {
return false;
return true;
export default FormHelper;
我们抽取出FormHelper来完成这个任务
import { Button, Input, Checkbox } from 'antd';
import { useState, useRef } from 'react';
import { FormLayout, FormItem } from '@formily/antd';
import FormHelper from './FormHelper';
type FormType = {
name: string;
export default () => {
const [state, setState] = useState(0);
const formData = useRef<FormType>({
name: '',
const currentFormData = formData.current;
return (
<FormLayout>
<FormItem
key={'1'}
label="Username"
asterisk={true}
feedbackStatus={FormHelper.getFeedbackStatus(
currentFormData,
'name',
feedbackText={FormHelper.getFeedbackText(
currentFormData,
'name',
<Input
//在Form下面的,不能使用defaultValue
key={FormHelper.getId(currentFormData, 'name')}
defaultValue={currentFormData.name}
onChange={(e) => {
currentFormData.name = e.target.value;
let { shouldRefresh } = FormHelper.validate(
currentFormData,
'name',
'required',
'number',
if (shouldRefresh) {
setState((v) => v + 1);
console.log(currentFormData);
</FormItem>
<Button
onClick={() => {
currentFormData.name = 'jj';
FormHelper.refreshId(currentFormData, 'name');
FormHelper.clearValidate(currentFormData, 'name');
setState((v) => v + 1);
{'设置'}
</Button>
<FormItem>
<Button type="primary" htmlType="submit">
Submit
</Button>
</FormItem>
</FormLayout>
这次的代码清爽多了,而且保证了可组合性,和适用性强。
14.4 表单Schema与校验分离组件
在前面的例子中,我们展示了一个问题是,表单校验的位置遍及到onChange,以及submit的位置,逻辑不够聚合,不容易被使用。另外FormItem和Input的代码之间,也比较多重复的代码。在这个基础上,我们提出使用FormBoost组件来优化一下。
type ValidatorFunctionType = (text: any) => string;
const BuiltInValidator = {
required: (text: any): string => {
if (text === undefined || text === '' || text === null) {
return '请输入';
} else {
return '';
number: (text: any): string => {
if (/^\d+$/.test(text)) {
return '';
} else {
return '请输入整数';
string: (text: any): string => {
if (typeof text == 'string') {
return '';
} else {
return '请输入字符串';
notEmpty: (data: any): string => {
if (typeof data == 'object' &&
data instanceof Array &&
data.length != 0) {
return '';
} else {
return '不能为空';
notNull: (data: any): string => {
if (typeof data == 'object' &&
data != null) {
return '';
} else {
return '不能为Null';
type ValidatorType = keyof typeof BuiltInValidator | ValidatorFunctionType;
type PropertySchemaType = {
[K in string]: FieldSchema;
class FieldSchema {
private checkers: ValidatorType[] = [];
public constructor(...checkers: ValidatorType[]) {
this.checkers = checkers;
public validateSelf(data: any): string {
let result = [];
for (let i in this.checkers) {
let singleChecker = this.checkers[i];
let singleCheckResult = '';
if (typeof singleChecker == 'string') {
singleCheckResult = BuiltInValidator[singleChecker](data);
} else {
singleCheckResult = singleChecker(data);
if (singleCheckResult != '') {
result.push(singleCheckResult);
return result.join(",");
public validate(data: any): string {
return this.validateSelf(data);
public getItemSchema(): FieldSchema {
throw new Error('不支持的getItemSchema');
public getPropertySchema(): PropertySchemaType {
throw new Error("不支持的getPropertySchema ");
class NormalSchema extends FieldSchema {
public constructor(...checkers: ValidatorType[]) {
super(...checkers);
class ArraySchema extends FieldSchema {
private itemSchema: FieldSchema;
public constructor(itemSchema: FieldSchema, ...checkers: ValidatorType[]) {
super(...checkers);
this.itemSchema = itemSchema;
public validate(data: any): string {
let superCheck = super.validate(data);
if (superCheck != '') {
return superCheck;
if (typeof data == 'undefined' || data == null) {
return '';
if (typeof data != 'object' && data instanceof Array == false) {
return '请输入数组';
for (let i in data) {
let single = data[i];
let itemCheck = this.itemSchema.validate(single);
if (itemCheck != '') {
return "->[" + i + "] " + itemCheck;
return "";
public getItemSchema(): FieldSchema {
return this.itemSchema;
class ObjectSchema extends FieldSchema {
private itemSchema: PropertySchemaType = {};
public constructor(itemSchema: PropertySchemaType, ...checkers: ValidatorType[]) {
super(...checkers);
this.itemSchema = itemSchema;
public validate(data: any): string {
let superCheck = super.validate(data);
if (superCheck != '') {
return superCheck;
if (typeof data == 'undefined' || data == null) {
return '';
if (typeof data != 'object') {
return '请输入对象';
for (let i in this.itemSchema) {
let value = data[i];
let propertySchema = this.itemSchema[i];
let itemCheck = propertySchema.validate(value);
if (itemCheck != '') {
return "->(" + i + ") " + itemCheck;
return "";
public getPropertySchema(): PropertySchemaType {
return this.itemSchema;
export {
FieldSchema,
NormalSchema,
ArraySchema,
ObjectSchema,
先定义Schema组件,以方便定义一个表单的格式应该是什么。
import { ArraySchema, FieldSchema, ObjectSchema } from "./FormSchema";
let globalId = 10001;
const getIdName = (key: string | number) => {
return '_' + key + '_id';
const getFeedbackName = (key: string | number) => {
return '_' + key + '_feedback';
type ValidateResult = {
shouldRefresh: boolean;
type ValidateAllResult = {
isValid: boolean;
message: string;
class FormChecker {
private schema: FieldSchema;
public constructor(schema: FieldSchema) {
this.schema = schema;
public getId<T, K extends keyof T>(target: T, key: K): number {
const current = target as any;
const keyName = getIdName(key as string);
let idValue = current[keyName];
if (idValue == undefined) {
idValue = globalId++;
current[keyName] = idValue;
return idValue;
public refreshId<T, K extends keyof T>(target: T, key: K): number {
const current = target as any;
const keyName = getIdName(key as string);
const idValue = globalId++;
current[keyName] = idValue;
return idValue;
public getFeedbackStatus<T, K extends keyof T>(
target: T,
key: K,
): 'error' | undefined {
const current = target as any;
const keyName = getFeedbackName(key as string);
let feedbackValue = current[keyName];
if (feedbackValue == undefined || feedbackValue == '') {
return undefined;
} else {
return 'error';
public getFeedbackText<T, K extends keyof T>(target: T, key: K): string {
const current = target as any;
const keyName = getFeedbackName(key as string);
let feedbackValue = current[keyName];
if (feedbackValue == undefined || feedbackValue == '') {
return '';
} else {
return feedbackValue as string;
public clearValidate<T, K extends keyof T>(target: T, key: K): ValidateResult {
//获取旧feedBack
let oldFeedBack = this.getFeedbackText(target, key);
//赋值新feedback
let current = target as any;
const keyName = getFeedbackName(key as string);
current[keyName] = undefined;
if (oldFeedBack == '') {
return { shouldRefresh: false };
} else {
return { shouldRefresh: true };
public validate<T, K extends keyof T>(
target: T,
key: K,
): ValidateResult & ValidateAllResult {
//获取旧feedBack
let oldFeedBack = this.getFeedbackText(target, key);
//计算newFeedBack
let current = target as any;
let propertySchemaAll = this.schema.getPropertySchema();
let propertySchema = propertySchemaAll[key as any];
if (!propertySchema) {
throw new Error("不存在的属性 " + key);
let newFeedBack = propertySchema.validateSelf(current[key]);
//赋值newFeedBack
const keyName = getFeedbackName(key as string);
current[keyName] = newFeedBack;
//返回是否该刷新
if (oldFeedBack != newFeedBack) {
return {
shouldRefresh: true,
isValid: newFeedBack == '',
message: newFeedBack,
} else {
return {
shouldRefresh: false,
isValid: newFeedBack == '',
message: newFeedBack,
public clearAllValidate<T>(target: T) {
if (this.schema instanceof ArraySchema) {
let itemChecker = new FormChecker(this.schema.getItemSchema());
if (typeof target == 'object' && target instanceof Array == true) {
for (let i in target) {
itemChecker.clearAllValidate(target[i]);
} else if (this.schema instanceof ObjectSchema) {
let propertySchemaAll = this.schema.getPropertySchema();
if (typeof target == 'object' && target instanceof Array == false) {
for (let key in propertySchemaAll) {
let itemChecker = new FormChecker(propertySchemaAll[key]);
const keyName = getFeedbackName(key as string);
(target as any)[keyName] = undefined;
//子清除
itemChecker.clearAllValidate((target as any)[key])
public validateAll<T>(target: T): ValidateAllResult {
if (this.schema instanceof ArraySchema) {
let itemChecker = new FormChecker(this.schema.getItemSchema());
if (typeof target == 'object' && target instanceof Array == true) {
let firstFail: ValidateAllResult = {
isValid: true,
message: '',
for (let i in target) {
let single = itemChecker.validateAll(target[i]);
if (single.isValid == false && firstFail.isValid == true) {
firstFail = {
isValid: false,
message: '->[' + i + "] " + single.message,
return firstFail;
} else if (this.schema instanceof ObjectSchema) {
let propertySchemaAll = this.schema.getPropertySchema();
if (typeof target == 'object' && target instanceof Array == false) {
let firstFail: ValidateAllResult = {
isValid: true,
message: '',
for (let key in propertySchemaAll) {
//校验自身字段
let single: ValidateAllResult = this.validate(target, key as any);
if (single.isValid == false && firstFail.isValid == true) {
firstFail = {
isValid: false,
message: '->(' + key + ") " + single.message,
//校验子字段
let childSchema = propertySchemaAll[key];
let childChecker = new FormChecker(childSchema);
single = childChecker.validateAll((target as any)[key]);
if (single.isValid == false && firstFail.isValid == true) {
firstFail = {
isValid: false,
message: '->(' + key + ") " + single.message,
return firstFail;
return {
isValid: true,
message: '',
export default FormChecker;
定义FormChecker组件,对一份Schema,以及一份Data针对性地进行校验,然后将结果输出到data的隐藏字段上面。
import { FormItem, IFormItemProps } from "@formily/antd"
import { ReactElement, cloneElement } from 'react';
import FormChecker from "./FormChecker";
const FieldBoost: <RecordType, K extends keyof RecordType>(props: IFormItemProps & {
children: ReactElement,
manualRefresh: () => void;
twoWayBind?: boolean,
onGetValue?: (e: any) => void;
onChange?: (e: any) => void;
data: RecordType,
dataIndex: K,
formChecker: FormChecker,
}) => ReactElement = (props) => {
const { children, manualRefresh, twoWayBind, onGetValue, onChange, data, dataIndex, formChecker, ...resetProps } = props;
const getValue = (e: any) => {
let value = e;
if (e && e.target) {
value = e.target.value;
data[dataIndex] = value;
const validateResult = formChecker.validate(data, dataIndex);
if (onGetValue) {
onGetValue(e);
return validateResult;
let newChildren: JSX.Element;
if (!twoWayBind) {
const newOnChange = onChange ? onChange : (e: any) => {
const { shouldRefresh } = getValue(e);
if (shouldRefresh) {
manualRefresh();
//绑定defaultValue
newChildren = cloneElement(children, {
key: formChecker.getId(data, dataIndex),
defaultValue: data[dataIndex],
onChange: newOnChange,
} else {
//绑定Value
const newOnChange = onChange ? onChange : (e: any) => {
getValue(e);
manualRefresh();
newChildren = cloneElement(children, {
value: data[dataIndex],
onChange: newOnChange,
return (<FormItem
feedbackStatus={formChecker.getFeedbackStatus(data, dataIndex)}
feedbackText={formChecker.getFeedbackText(data, dataIndex)}
{...resetProps}
{newChildren}
</FormItem>);
export default FieldBoost;
FieldBoost组件就是使用了FormChecker组件,并且绑定了FormItem和输入组件的value与onChange。默认绑定的方式为key+defaultValue+onChange,性能最好。还有一种方法是双向绑定,绑定value与onChange,性能稍差一点,特别是大表单。
import { Button, Input, Checkbox } from 'antd';
import { useState, useRef } from 'react';
import { FormLayout, FormItem } from '@formily/antd';
import { NormalSchema, ObjectSchema } from './FormBoost/FormSchema';
import FieldBoost from './FormBoost/FieldBoost';
import FormChecker from './FormBoost/FormChecker';
const formSchema = new ObjectSchema({
name: new NormalSchema('required'),
age: new NormalSchema('required', 'number'),
}, 'required');
const formChecker = new FormChecker(formSchema);
type FormType = {
name?: string;
age?: number;
export default () => {
const [state, setState] = useState(0);
const manualRefresh = () => {
setState((v) => v + 1);
const formData = useRef<FormType>({});
const currentFormData = formData.current;
return (
<FormLayout>
<FieldBoost<typeof currentFormData, keyof typeof currentFormData>
label="Username"
asterisk={true}
formChecker={formChecker}
data={currentFormData}
dataIndex='name'
manualRefresh={manualRefresh}>
<Input />
</FieldBoost>
<FieldBoost<typeof currentFormData, keyof typeof currentFormData>
label="Age"
asterisk={true}
formChecker={formChecker}
data={currentFormData}
dataIndex='age'
manualRefresh={manualRefresh}>
<Input />
</FieldBoost>
<Button
onClick={() => {
currentFormData.name = 'jj';
formChecker.refreshId(currentFormData, 'name');
formChecker.validateAll(currentFormData);
manualRefresh();
{'设置'}
</Button>
<FormItem>
<Button type="primary" htmlType="submit">
Submit
</Button>
</FormItem>
</FormLayout>
使用方式,代码更加清爽了,而且也比较好看。为了提高FieldBoost的通用性,可以对FieldBoost组件加入不绑定输入组件的value与onChange属性的方法。
20 FAQ
20.1 表格组件的scrollX
表格组件的scroll-x打开以后,需要保证表格组件的外部没有FormItem,否则它的width:100%,默认配置会导致,表格的宽度超出屏幕的宽度。
解决方法有三种:
将FormItem的title设置为空
将FormItem去掉,不要了
将FormItem的item-control下面,加入position:relative的CSS配置,(这点未严格测试,但应该可以)
20.2 样式无法显示
当使用formily-antd,或者antdesign-pro的时候无法自动引入对应的样式文件,这是因为,umi难以自动从node_modules中分析出依赖了哪些样式文件。
解决办法有两种:
直接引入’antd/dist/antd.compact.css’文件,这样不能支持样式变量更改。
在项目中手动import你自己用到的哪些控件,以显式告诉umi你依赖了antd的哪些控件,这样能支持样式变量更改。
21 总结
AntDesign的总体设计合理而且优雅,基本能覆盖绝大部分的后台管理系统的页面设计。我们学习AntDesign的关键在于,每一个子组件的用法是什么,那么每个页面就是一个简单的组件组合操作就可以了。
在学习过程中,印象较为深刻的地方是:
ProLayout与UMI路由,相当松耦合的设计,ProLayout只处理UI展示,UMI路由只处理路由变化时的children是什么,两者轻松地通过受控操作组合在一起使用。
StatisticCard的API设计,从底层的Statistic,到中层的StatisticCard,到最后的StatisticCard.Group,三者不同的API设计最终实现了灵活的统计页面展示。
ProLayout,Flex布局,栅栏布局,分栏布局,一个组件全部搞定,省事。
Card与List,轻松与简单的API设计,覆盖了绝大部分的场景,而且不需要写任何CSS,实用且方便。
无邪的楼房 · 张豆豆(中国艺术体操运动员)_百度百科 4 月前 |