AntDesign经验汇总

2021-07-26 fishedee 前端

0 概述

Ant Design是较为完整的React组件库。我们平时开发UI都是div+flex+css使用到底的,这其实是一种低层次的抽象。组件库的目的的是,让开发者的抽象提高上来,接触的不是div,而是List,Card,Tab这样的组件,尽量避免使用css和flex,而是要考虑如何用现有的组件去实现它。在学习的过程中,我们要注意,不同组件之间的抽象的共性,例如,Ant Design的组件都有title,extra,footer,icon,shape,children的这些共同的抽象,尝试理解这些共性,才能更好地使用它。

一个后台管理系统的组件库,它主要包括以下几个模块:

  • 页面布局组件,ProCard,PageContainer,ProLayout
  • 基础展示组件,Button,Badge,Tag,Card,Statistic
  • 数组展示组件,TreeSelect,List,Table
  • 输入组件,Input,InputNumber,CheckBox,Select,DatePicker等等。

由于在Formily中已经介绍过输入组件了,这篇文章主要介绍Antd的页面布局组件与展示组件。

ant design的官方文档在这里,ant design Pro Components的官方文档在这里

1 构建与配置

1.1 构建

代码在这里

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 (
        <div>
            <DatePicker onChange={handleChange} />
            <Table dataSource={products} columns={columns} />
        </div>
    );
};

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的布局相当方便灵活,基本能解决大部分的布局问题。

2.1 基础,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>
        </div>
    );
};

基础使用,这点没啥好说的

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>
        </div>
    );
};

这点也不太难

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>
        </div>
    );
};

标签页的每个内容写在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>
                    Auto
                </ProCard>
            </ProCard>
            <ProCard style={{ marginTop: 8 }} gutter={8} ghost>
                <ProCard bordered layout="center">
                    Auto
                </ProCard>
                <ProCard
                    colSpan="30%" //colSpan可以为单独的比例,而不是数字
                    bordered
                >
                    colSpan - 30%
                </ProCard>
            </ProCard>
        </div>
    );
};

只需要在外层套一个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>
        </div>
    );
};

与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 (
        <div
            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>
        </div>
    );
};

这个时候用ProCard下面嵌套有Divider就可以实现这种微分割线的分栏布局。注意,外层的ProCard是用ProCard.Group还是ProCard,差异不大,可以混着用。注意,Divider是用ProCard的Divider,不是AntDesign的Divider。

3 单页面布局组件,PageContainer

代码在这里

PageContainer描述的是一个页面常用的展示方式

3.1 基础,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中,不同页面指向到不同的标签页内容。

3.2 面包屑与可控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 (
        <div
            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>
                                    <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>
        </div>
    );
};

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 (
        <div
            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) => (
                    <div
                        id="customize_menu_header"
                        onClick={() => {
                            window.open('https://remaxjs.org/');
                        }}
                    >
                        {logo}
                        {title}
                    </div>
                )}
                //左侧菜单栏底部的footer
                menuFooterRender={(props) => {
                    return (
                        <a
                            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"
                        >
                            <img
                                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'}
                        </a>
                    );
                }}
                //左侧菜单栏的每个菜单项的渲染
                menuItemRender={(item, dom) => (
                    //每个表单项的包装器,可以设置点击时的触发行为
                    <a
                        onClick={() => {
                            setPathname(item.path || '/welcome');
                        }}
                    >
                        {dom}
                    </a>
                )}
                //右侧内容的展示
                rightContentRender={() => (
                    <div>
                        <Avatar
                            shape="square"
                            size="small"
                            icon={<UserOutlined />}
                        />
                    </div>
                )}
                //内容的页脚
                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>,
                    ]}
                >
                    <div
                        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>}
                        />
                    </div>
                </PageContainer>
            </ProLayout>
            <SettingDrawer
                //浮层,用来动态调整Menu的属性
                //在实际环境不需要用
                pathname={pathname}
                getContainer={() => document.getElementById('test-pro-layout')}
                settings={settings}
                onSettingChange={(changeSetting) => {
                    setSetting(changeSetting);
                }}
                disableUrlParams
            />
        </div>
    );
};

代码也是比较显然的,注意点如下:

  • 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="欢迎使用">
            <div
                style={{
                    height: '120vh',
                }}
            >
                Hello World
            </div>
        </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 (
        <div
            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 (
                        <a
                            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"
                        >
                            <img
                                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'}
                        </a>
                    );
                }}
                //左侧菜单栏的每个菜单项的渲染
                menuItemRender={(item, dom) => (
                    //每个表单项的包装器,可以设置点击时的触发行为
                    <a
                        onClick={() => {
                            history.push(item.path || '/welcome');
                        }}
                    >
                        {dom}
                    </a>
                )}
                //设置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>
        </div>
    );
};

首先,我们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>
                                <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>
                                <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>
                                <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>
                    <Tag
                        closable
                        onClose={(e) => {
                            e.preventDefault();
                            console.log('close');
                        }}
                    >
                        Tag 2
                    </Tag>
                </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>
                    <Tag icon={<SyncOutlined spin />} color="processing">
                        processing
                    </Tag>
                </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}>
                    <Card
                        title="我是标题"
                        extra="我是extra"
                        style={{ width: 300 }}
                        cover={
                            <img
                                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={
                            <img
                                style={imgStyle}
                                src="https://gw.alipayobjects.com/mdn/rms_7bc6d8/afts/img/A*dr_0RKvVzVwAAAAAAAAAAABkARQnAQ"
                                alt="icon"
                            />
                        }
                    />
                    <Statistic
                        layout="vertical"
                        title={'访客数'}
                        value={475}
                        icon={
                            <img
                                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的,不需要显式传入也可以。

10.3 统计卡片组件,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={
                            <>
                                <img
                                    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={
                            <img
                                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={
                            <img
                                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={
                            <img
                                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={
                            <img
                                src="https://gw.alipayobjects.com/zos/alicdn/ShNDpDTik/huan.svg"
                                alt="百分比"
                                width="100%"
                            />
                        }
                        //将图表放在统计信息的左边
                        chartPlacement="left"
                    />
                </Space>
            </ProCard>
        </Space>
    );
};

代码还是简单的,注意footer与children嵌套Statistic的用法,这个还是挺好用的。

10.4 统计卡片分组组件,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={
                        <img
                            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={
                        <img
                            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 (
        <div>
            <Tree
                showLine={true}
                showIcon={false}
                defaultExpandedKeys={['0-0', '0-1']}
                //defaultExpandParent={true}
                onSelect={onSelect}
                //传入是一个数组
                treeData={treeData}
            />
        </div>
    );
};

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>
                <List
                    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>
                <List
                    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={
                                <img
                                    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}
                                    </a>
                                }
                                //描述
                                description="Ant Design, a design language for background applications, is refined by Ant UED Team"
                            />
                            <div
                            //children的内容
                            >
                                content
                            </div>
                        </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>
                <List
                    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={
                                <img
                                    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}
                                    </a>
                                }
                                //描述
                                description="Ant Design, a design language for background applications, is refined by Ant UED Team"
                            />
                            <div
                            //children的内容
                            >
                                content
                            </div>
                        </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()}
                        </Tag>
                    );
                })}
            </>
        ),
    },
    {
        title: 'Action',
        key: 'action',
        render: (text: string, record: any, index: number) => (
            <Space size="middle">
                <a>
                    Invite {record.name},{index + 1}
                </a>
                <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是什么。

13.2 头部,尾部与边框,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()}
                        </Tag>
                    );
                })}
            </>
        ),
    },
    {
        title: 'Action',
        key: 'action',
        render: (text: string, record: any, index: number) => (
            <Space size="middle">
                <a>
                    Invite {record.name},{index + 1}
                </a>
                <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构成了选择行的受控操作

13.4 树型数据的展开操作,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>
    );
};

这点也是简单的。

13.5 特定数据的展开操作,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>
        <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: () => (
                    <span>
                        <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}>
                            <a>
                                More <DownOutlined />
                            </a>
                        </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属性就可以

13.9 合并列与合并行,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,
    Tag,
    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 (
        <Form
            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 = '';
                        if (
                            !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,实用且方便。

最后,检验AntDesign的成果在于,看一看以上的页面,如何用AntDesign来做出来,它们是如何由基础组件组合起来的。

相关文章