React入门:在 React 中使用使用 Antd

Antd 使用学习

Antd 使用学习1. 配置antd 配合 craco2. 使用Antd1. Antd 使用第一课 Form 表单扩展知识:2. Table 表格的使用:3. Select 与 Input4. Dropdown 与 Menu5. Typegraphy.Text 用于显示一段文字6. Button7. Popover8. List,List.Item,List.Item.Meta9. Modal 模态弹窗10. Spin 显示加载动画效果

1. 配置antd 配合 craco

  1. 安装craco yarn add @craco/craco,并修改 package.json 文件
  2. 安装craco-less yarn add craco-less
  3. 安装bable-plugin-import yarn add babel-plugin-import 用于按需映入css or less

修改package.json

1
2
3
4
5
6
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},

根目录创建配置文件craco.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const CracoLessPlugin = require("craco-less");
// 需要安装两个插件:`yarn add craco-less` `yarn add babel-plugin-import` 从而实现样式的按需引入
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { "@primary-color": "#1DA57A" },
javascriptEnabled: true,
},
},
},
},
],
babel: {
//支持装饰器
plugins: [
[
"import",
{
libraryName: "antd",
libraryDirectory: "es",
style: true, //设置为true即是less 这里用的是css,如果是scss,则需要设置为false,否则不能正常生效
},
],
],
},
};

2. 使用Antd

1. Antd 使用第一课 Form 表单

  1. 引入 import { Form, Input, Button } from 'antd';

  2. 使用

    标签,每一个表单项使用 <**Form.Item**>

  3. Item 标签中需要填写 name= "" 这个字段将作为表单提交事件的属性名

  4. item 可以增加 rules 属性,例如:rules={[{ required: true, message: '请输入密码' }]}

  5. 表单的 submit 写法不同于 html 原生,antd 的 type是用于指定 组件样式的,是一个字面量类型

    1
    2
    declare const ButtonTypes: ["default", "primary", "ghost", "dashed", "link", "text"];
    export declare type ButtonType = typeof ButtonTypes[number];

    需要使用 htmlType 属性来指定为 submit

  6. 最后 表单的 提交事件也不再是 onSubmit,而是onFinish

  7. 表单事件注册的函数也不同于,原生html,接受的不再是react的event事件,而是封装好的 item指定名字的对象

最终代码对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//使用antd
import { useAuth } from 'context/auth-context';
import { Form, Input, Button } from 'antd';

const LoginScreen = () => {
const { login, user } = useAuth()
//根据 Form.Item的name决定的这个values是什么
const handleSubmit = (values: { username: string, password: string }) => {
const { username, password } = values
login({ username, password })
}

return (
<Form onFinish={handleSubmit} >
{user ? <div>登录成功,用户名为:{user?.name}</div> : null}
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
<Input placeholder='用户名' type="text" id="username" />
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
<Input placeholder='密码' type="password" id="password" />
</Form.Item>
<Form.Item>
<Button htmlType='submit' type='primary'>登录</Button>
</Form.Item>
</Form>
)
}

export default LoginScreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//不使用antd
import { FormEvent } from 'react'
import { useAuth } from 'context/auth-context';

const LoginScreen = () => {
const { login, user } = useAuth()
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
//阻止默认行为
e.preventDefault()
console.log('submit')
const username = (e.currentTarget.elements[0] as HTMLInputElement).value
const password = (e.currentTarget.elements[1] as HTMLInputElement).value
login({ username, password })
}
return (
<form onSubmit={handleSubmit}>
{user ? <div>登录成功,用户名为:{user?.name}</div> : null}
<div>
<label htmlFor="username">Username</label>
<input type="text" id="username" />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" id="password" />
</div>
<button type='submit'>登录</button>
</form>
)
}

export default LoginScreen

可以看出 antd 省去了我们很多的操作。

知识点,Form.Item 代理了其子组件中的 value onChange函数,并用自己的 name 为这个内容命名,使其成为了受控组件,这个我之后会单开一个文章详细介绍。

扩展知识:

const [form] = Form.useForm(),可以将这个钩子的返回值赋值给 Form 的 form,用于控制Form表单的行为;

  • form.resetFields() 清空表单
  • form.setFieldsValue(editingProject) 使用一个对象填充表单

使用该钩子可能会导致一个异常:

1
react_devtools_backend.js:3973 Warning: Instance created by `useForm` is not connected to any Form element. Forget to pass `form` prop?

一般来说无视即可,如果觉得是在讨厌,可以在Form的父组件 传入 forceRender={true}

2. Table 表格的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { Table } from 'antd';
import React, { PropsWithChildren } from 'react'
import { User } from './search-panel';

type Project = {
id: string,
name: string,
personId: string,
}
type ListProps = {
list: Project[];
users: User[]
}
const List: React.FC<PropsWithChildren<ListProps>> = ({ list, users }) => {
return (
<Table
//加载状态
loading={false}
//数据行的key
rowKey="id"
// 是否分页
pagination={false}
//数据源
dataSource={list}
//渲染的列
columns={[
{
title: '项目名称',
dataIndex: 'name',
sorter: (a, b) => a.name.localeCompare(b.name)
},
{
title: '项目负责人',
render(value, project) {
return <span key={project.id}> {users.find(user => user.id === project.personId)?.name || "未知"}</span>
}
}]} />
)
}

export default List

这里由于负责人我们拿到的只有id,需要我们从user列表中去自行计算,所以直接传入render函数;

dataIndex,表示的是我们使用的这个key中的数据去渲染这一行;

sorter 用于排序,传入一个排序算法;

如果我们的组件是对Table的封装,我们可以透传table的props,也就是让我们组件扩展Table的Props

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { Table, TableProps } from 'antd';
import dayjs from 'dayjs';
import React, { PropsWithChildren } from 'react'
import { User } from './search-panel';

type Project = {
id: string,
name: string,
personId: string,
pin: boolean,
organization: string,
created: number;
}
//扩展TableProps,增加users
type ListProps = {
users: User[]
} & TableProps<Project>

const List: React.FC<PropsWithChildren<ListProps>> = ({ users, ...props }) => {
return (
<Table
//透传 antd的props
{...props}
//每一行的key
rowKey="id"
// 是否分页
pagination={false}
//渲染的列
columns={[
{
title: '项目名称',
dataIndex: 'name',
sorter: (a, b) => a.name.localeCompare(b.name)
},
{
title: '部门',
dataIndex: 'organization'
},
{
title: '项目负责人',
render(_, project) {
return <span key={project.id}> {users.find(user => user.id === project.personId)?.name || "未知"}</span>
}
},
{
title: '创建时间',
render(_, project) {
return <span>
{project.created ? dayjs(project.created).format("YYYY-MM-DD") : '未知时间'}
</span>
},
},]} />
)
}

export default List

这样上层使用我们的封装组件时,可以方便的传入Table原有的Props

3. Select 与 Input

input输入框的用法与原来大致相同,Select 的用法有差别

1
2
3
4
5
6
7
8
9
10
11
12
<Select
value={param.personId}
onChange={(value) => setParam({ ...param, personId: value })}>

<Select.Option value="">负责人</Select.Option>

{users.map((user: User) => (
<Select.Option key={user.id} value={user.id}>
{user.name}
</Select.Option>
))}
</Select>
  1. 回调事件与Form类似 直接可以拿到value
  2. 子项目需使用 <Select.Option>
  3. value属性用于指定当前显示的option

4. Dropdown 与 Menu

1
2
3
4
5
6
7
8
9
<Dropdown overlay={(
<Menu>
<Menu.Item key='logout'>
<a onClick={() => logout()}>退出</a>
</Menu.Item>
</Menu>
)}>
<a onClick={e => e.preventDefault()}>Hi,{user?.name}</a>
</Dropdown>

Dropdown 必须包含一个 overlay 的props,其中返回的是JSX组件,一般使用 Menu,当鼠标悬停于 Dropdown 组件时,会显示其中的overlay中的组件。

Dropdown 的子组件,就是当前显示的组件样式;

image-20220509171701248.png

5. Typegraphy.Text 用于显示一段文字

显示一行错误信息:

1
{error ? <Typography.Text type='danger'>{error.message}</Typography.Text> : null}

6. Button

Button是AntD中最常用的组件之一,通过Type可以设定各种用途

1
2
3
4
5
6
7
8
//将子组件作为link
<Button type="link"></Button>
//使用emotion包装的AntD Button
export const LongButton = styled(Button)`
width: 100%;
`
//设置 htmlType loading 等props
<LongButton loading={isLoading} htmlType='submit' type='primary' >注册</LongButton>

在AntD中 Button 共有6中type

1
2
declare const ButtonTypes: ["default", "primary", "ghost", "dashed", "link", "text"];
export declare type ButtonType = typeof ButtonTypes[number];

我们在原生表单中,往往使用type=‘submit’来表示提交按钮,在AntD组件中 需要使用:htmlType,来指定该动作

当我们使用Button 作为link 包裹组件时,子组件有时候并不能居中排版,我们可以使用flex的行内样式来设置

1
2
3
<Button type="link" onClick={resetRoute} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<SoftwareLogo width={'18rem'} color={'rgb(38,132,255)'} />
</Button>

如果使用了emotion,可以使用自定义样式的emotion组件来封装,例如一个没有padding的Button:

1
2
3
export const ButtonNoPadding = styled(Button)`
padding: 0;
`

常用props:

  • type 设置样式
  • htmlType 用于在表单中设置为提交
  • loading 显示加载

7. Popover

Popover 与 Dropdown很像,但是又有不同之处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import styled from '@emotion/styled'
import { Button, Divider, List, Popover, Typography } from 'antd'
import React from 'react'
import { useProjects } from 'utils/project'
import { ButtonNoPadding } from './lib'

export const ProjectPopover = ({ setProjectModalOpen }: { setProjectModalOpen: (isOpen: boolean) => void }) => {
//获取全部项目
const { data: projects, isLoading } = useProjects()
//筛选pin为true的项目
const pinnedProjects = projects?.filter(project => project.pin)

const content = <Container>
<Typography.Text type='secondary'>收藏项目</Typography.Text>
<List>
{
pinnedProjects?.map(project => (
<List.Item key={project.id}>
<List.Item.Meta title={project.name} />
</List.Item>
))
}
</List>
<Divider />
<ButtonNoPadding type='link' onClick={() => setProjectModalOpen(true)}>创建项目</ButtonNoPadding>
</Container>
return (
<Popover
placement='bottom'
content={content}
>
<span>项目</span>
</Popover>
)
}

const Container = styled.div`
min-width: 30rem;
`

Popover 通过 placement控制显示的位置,通过content设置显示的内容:

8. List,List.Item,List.Item.Meta

1
2
3
4
5
6
7
8
9
<List>
{
pinnedProjects?.map(project => (
<List.Item key={project.id}>
<List.Item.Meta title={project.name} />
</List.Item>
))
}
</List>

用于显示一个简单的列表

9. Modal 模态弹窗

1
2
3
4
5
6
7
8
9
10
11
const confirmDelete = () => {
Modal.confirm({
title: '确认删除',
content: '确认删除该项目吗?',
okText: '确认',
cancelText: '取消',
onOk: () => {
deleteKanban({ id: kanban.id })
}
})
}

除了在方法里调用弹窗,还可以作为一种布局使用,例如一个编辑任务的模态窗口布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

import { Button, Input, Modal } from 'antd'
import Form from 'antd/lib/form'
import { TaskTypeSelect } from 'components/task-type-select'
import { UserSelect } from 'components/user-select'
import { useEffect } from 'react'
import { useDeleteTask, useEditTask } from 'utils/task'
import { useTaskModal, useTasksQueryKey } from './util'

const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
}

export const TaskModal = () => {
const [form] = Form.useForm()
const { editingTaskId, editingTask, close } = useTaskModal()
const { mutateAsync: editTask, isLoading: editLoading } = useEditTask(useTasksQueryKey())

const { mutateAsync: deleteTask } = useDeleteTask(useTasksQueryKey())

const onCancel = () => {
close()
form.resetFields()
}

const onOk = async () => {
await editTask({ ...editingTask, ...form.getFieldsValue() })
onCancel()
}
useEffect(() => {
form.setFieldsValue(editingTask)
}, [form, editingTask])

const confirmDelete = () => {
Modal.confirm({
title: '确认删除',
content: '确认删除该任务吗?',
okText: '确认',
cancelText: '取消',
onOk: () => {
deleteTask({ id: Number(editingTaskId) })
close()
}
})
}
return (
<Modal
forceRender={true}
onCancel={close}
onOk={onOk}
okText='确认'
cancelText='取消'
confirmLoading={editLoading}
visible={!!editingTaskId}
title='编辑任务'
>
<Form
{...layout}
initialValues={editingTask}
form={form}
>
<Form.Item
label="任务名"
name={'name'}
rules={[{ required: true, message: '请输入任务名' }]}
>
<Input />
</Form.Item>
<Form.Item
label="经办人"
name={'processorId'}
>
<UserSelect style={{ width: '100%' }} defaultOptionName='经办人' />
</Form.Item>
<Form.Item
label="类型"
name={'typeId'}
rules={[{ required: true, message: '请选择类型' }]}
>
<TaskTypeSelect style={{ width: '100%' }} />
</Form.Item>

</Form>
<div style={{ 'textAlign': 'right' }}>
<Button
onClick={() => confirmDelete()}
type={'primary'}
danger
>
删除
</Button>
</div>
</Modal>
)
}

模态窗口布局组件一般放在需要用的页面的根节点,通过visible字段来进行控制显隐。

常用属性:

  • onCancel={close} 取消回调
  • onOk={onOk} 确认回调
  • okText=’确认’ 确认按钮的文字
  • cancelText=’取消’ 取消按钮的文字
  • confirmLoading={editLoading} 确认的loading
  • visible={!!editingTaskId} 控制显隐
  • title=’编辑任务’ 标题

10. Spin 显示加载动画效果

1
<Spin size='large' />