入门:在 React 中使用使用 Antd

576 阅读6分钟

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

  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "react-scripts eject"
  },

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

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是用于指定 组件样式的,是一个字面量类型

    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指定名字的对象

最终代码对比:

//使用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
//不使用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) 使用一个对象填充表单

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

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 表格的使用:

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

例如:

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 的用法有差别

                <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

                <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 用于显示一段文字

显示一行错误信息:

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

6. Button

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

//将子组件作为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

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

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

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

<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:

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

常用props:

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

7. Popover

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

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设置显示的内容:

image-20220509171619918

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

        <List>
            {
                pinnedProjects?.map(project => (
                    <List.Item key={project.id}>
                        <List.Item.Meta title={project.name} />
                    </List.Item>
                ))
            }
        </List>

用于显示一个简单的列表

9. Modal 模态弹窗

    const confirmDelete = () => {
        Modal.confirm({
            title: '确认删除',
            content: '确认删除该项目吗?',
            okText: '确认',
            cancelText: '取消',
            onOk: () => {
                deleteKanban({ id: kanban.id })
            }
        })
    }

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

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 显示加载动画效果

<Spin size='large' />