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来做出来,它们是如何由基础组件组合起来的。
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!