Antd 系列:antd 的一些日常需求实现

606 阅读5分钟

logo

持续更新中 2025-2-20

1. 既能输入不存在的选项又能进行下拉框选择

展开

你需要的是 ant.design ~ auto-complete

2. menu 组件无法动态设置 defaultSelectedKeys

image.png

展开

何时需要动态修改 defaultSelectedKeys?删除默认选中的菜单,需要让第一个菜单选中。

改变 defaultSelectedKeys 是无效的,因为它只有在首次渲染才会生效,我们需要使用到 selectedKeys。

// MenuWrapper.tsx
const [selectedKeys, setSelectedKeys] = React.useState<string[]>();

React.useEffect(() => {
    // 修复:当defaultSelectedKeys变化时,选中的菜单项没有变化,因为 defaultSelectedKeys 只有初次渲染时生效
    // 当外界更新 defaultSelectedKeys 时,需要手动更新 selectedKeys
    // 如此设计是为了简化,否则调用者需要同时提供 defaultSelectedKeys 和 selectedKeys
    setSelectedKeys(defaultSelectedKeys);
}, [defaultSelectedKeys]);

return <Menu
  defaultSelectedKeys={defaultSelectedKeys}
  selectedKeys={selectedKeys}
  onSelect={({ key }) => setSelectedKeys([key])}
/>
完整代码
<Menu
  className={styles.menu}
  style={{ width: 200, border: 0 }}
  defaultSelectedKeys={defaultSelectedKeys}
  selectedKeys={selectedKeys}
  defaultOpenKeys={defaultOpenKeys}
  onSelect={({ key }) => setSelectedKeys([key])}
  expandIcon={() => null}
  mode={'inline'}
  inlineIndent={8}
  items={items}
/>

能否只用 selectedKeys?不能,否则用户点击菜单的时候不能选中了,此时还得将 onSelect 暴露给调用方让其主动设置 selectedKeys,而这里我们只需要暴露 defaultSelectedKeys 即可,用 useEffect 做同步。故需结合 defaultSelectedKeysselectedKeys 一起使用。

使用更简单,内部处理了菜单点击高亮逻辑,只需传入 defaultSelectedKeys

<MenuWrapper defaultSelectedKeys={[...]} />

如果只用 selectedKeys 调用复杂:

<MenuWrapper selectedKeys={[...]} onSelect={onSelect} defaultSelectedKeys={[...]} />

3. 顶部带有搜索表单的表格,如何做到输入变化就自动发起搜索

展开

答案:onValuesChange 结合 debounce

<Form
  onValuesChange={() => {
    queryWithConditions({ pageNumber: current, pageSize });
  }}
  ...
 >
  ...
</Form>

// buttons 搜索 重置 新增等

// 主体内容 <Table

但是还不够,我们得做 debounce 省去无谓请求。

我们将之前的 queryWithConditions 重命名成 queryWithConditionsCore,然后将其包裹在 useDebounceFn,其他都不用变。

import { useDebounceFn } from 'ahooks';

// 组件内代码
const { run: queryWithConditions } = useDebounceFn(queryWithConditionsCore, { wait: 500 });

4. ProTable 如何让搜索按钮和自定义按钮比如“创建”同一行

展开

官方文档潦草了事,咋整?

image.png

如果你只是想新增按钮,直接一行代码搞定

optionRender: (searchConfig, props, dom) => dom.concat(creatingNode),

如果需要对“重置”或“搜索”按钮做调整可以如此操作:

search: {
  span: 4,
  labelWidth: 0,

  // 将搜索按钮和创建按钮放到同一行
  optionRender: ({ searchText, resetText }, { form }, [_resetBtn, _searchBtn]) => [
    // 重置按钮
    <Button
      key="reset"
      onClick={() => {
        form?.resetFields();
        form?.submit();
      }}
    >
      {resetText}
    </Button>,
    
    // 搜索按钮
    <Button
      key="sub"
      onClick={() => {
        form?.submit();
      }}
      
      // 给查询按钮增加 type 和 ghost 让其和新增按钮区分开来
      type="primary"
      ghost
    >
      {searchText}
    </Button>,

    // 新增按钮
    <Button
      key="button"
      type="primary"
      onClick={() => {
        history.push({
          pathname: '/foo/create'
        });
      }}
    >
      {t('common.action.create')}
    </Button>,
  ],
}

若并未对重置按钮做任何修改,可以直接复用 resetBtn

...

  // 将搜索按钮和创建按钮放到同一行
  optionRender: ({ searchText }, { form }, [resetBtn, _searchBtn]) => [
    // 重置按钮原封不动
    resetBtn,
    
    ...

如果只是为了微调,比如给查询按钮新增 prop ghost 可以使用 React.cloneElement

optionRender: (_, config, [resetBtn, queryBtn]) => {
  return [
    resetBtn, 
    
    // @ts-expect-error 新增 prop `ghost` 
    React.cloneElement(queryBtn, { ghost: true }),
  ];
},
参考

5. 如何识别当前项目运行的 antd 版本

展开
import { version } from 'antd';

console.log('当前Ant Design版本:', version); // 4.x.x 5.x.x

6. Radio 需要同时传输 name 和 id 给服务端如何实现

展开

hidden 表单的妙用。

hidden 表单也会提交给服务端,只是 UI 上不可见,刚好满足我们的需求,这也是以前 web 的常规技巧比如 csrf_token 的提交。

<Form.Item
    label={'资源组')}
    name="name"
    required
  >
    <Radio.Group
      options={nameOpts}
      optionType="button"
      
      // 🔥 1. 同步更新 name 对应的 id
      onChange={() => {
        const name = form.getFieldValue('name');

        setHiddenFieldForId(name);
      }}
    >
      <Radio />
  </Radio.Group>
</Form.Item>

<!-- 🔥 2. hidden 表单也会提交给服务端,只是 UI 上不可见,刚好满足我们的需求 -->
<Form.Item name="id" hidden>
    <Input />
</Form.Item>

如果有默认值需要初始化一次

  function setHiddenFieldForId(name: string) {
    const id = nameOpts.find(item => item.label === name)?.id;

    form.setFieldValue('id', id);
  }

  const initialId = nameOpts?.[0]?.value;
  if (initialId && !form.getFieldValue('id')) {
    setHiddenFieldForId(initialId);
  }

7. 没有修改,保存按钮需置灰,如何实现

展开

方案1:利用 Form 的 onValuesChange 检测变动即可。

文档:ant-design.antgroup.com/components/…

8. 没有搜索按钮,输入即可发起请求,如何实现

展开

第一感觉是用 FormonChange 但是如果开启 allowClear 用户点击清除按钮该事件不会触发,需要用 onValuesChange。当然更好是叠加 debounce

其实 antd 并没有 onChange 文档,但该事件确实存在。

import { PlusOutlined } from '@ant-design/icons';
import { useDebounceFn } from 'ahooks';
import { Form, Input, Space, Button } from 'antd';
import React from 'react';

interface IProps {
  onCreate: () => void;
  onChange: (params: { name: string; label: string }) => void;
}

export const SearchFilter: React.FC<IProps> = ({ onCreate, onChange }) => {
  const [form] = Form.useForm();
  // 🔥 关键点 2
  const { run: onFormChange } = useDebounceFn(
    () => {
      const values = form.getFieldsValue();
      console.log('SearchFilter onChange', values);

      onChange(values);
    },
    { wait: 600 },
  );

  return (
    <div style={{ display: 'flex', justifyContent: 'space-between' }}>
      <Form
        // 🔥 关键点 1
        onValuesChange={onFormChange}
        layout="inline"
        form={form}
        style={{ marginBottom: 24, flex: '1 0 auto' }}
      >
        <Form.Item name={'name'}>
          <Input style={{ width: 328 }} maxLength={200} placeholder="请输入名称" allowClear />
        </Form.Item>

        <Form.Item name={'label'}>
          <Input style={{ width: 328 }} maxLength={200} placeholder="请输入标签" allowClear />
        </Form.Item>
      </Form>

      <Space size={'middle'} style={{ alignItems: 'start' }}>
        <Button icon={<PlusOutlined />} onClick={onCreate} type="primary">
          创建
        </Button>
      </Space>
    </div>
  );
};

9. 一行排列的组件中间用垂直分隔线隔开如何实现

image.png

展开

9.1 通过 index > 0 使得首尾没有分割线。

import React from 'react';
import { Divider } from 'antd';

export type IFeeUI = { title: string; };

interface IProps {
  list: IFeeUI[];
}

export const ListWithSeparator: React.FC<IProps> = ({ list }) => {
  return (
    <div style={{ display: 'flex',  gap: 40, flexWrap: 'wrap', }} >
      {list.map((item, index) => (
          // 🔥 1. React.Fragment 包裹多个组件
          <React.Fragment key={item.id}>
            {/* 🔥 2. 通过 index 判断使得开头没有分割线 */}
            {index > 0 && (
              <Divider type="vertical" style={{ margin: 0, alignSelf: 'center' }} />
            )}
              
            <Component data={item}> 组件 </Component>
          </React.Fragment>
        ))}
    </div>
  );
};

9.2 forEach 结合 pop

详见 React 如何在组件间插入分隔线

10. 如何修改 popover / tooltip 箭头颜色

答案:通过 CSS 变量。该方式适用于很多 antd 样式覆盖场景

CSS 变量可以让自定义 CSS 变得非常容易无需再写 !important重复多个类名增加权重的代码,建议大家团队内组件库也如此设计。

第一步:找到 CSS 变量

image.png

点击变量可以发现是写在 class 上的:

image.png

第二步:给其父元素增加 CSS 变量

'--antd-arrow-background-color': '#4b6de8'

因为 CSS 变量的查找也是有作用域的,可以理解为“就近原则”,antd 默认变量都定义在对应的 class 上,或则 :root 上,如果我们的 CSS 变量定义在 style 上则权重会更高。

<Tooltip
  styles={{
    root: {
      // @ts-expect-error 通过 css 变量修改箭头颜色
      ['--antd-arrow-background-color']: '#4b6de8',
    },
    body: {
      ...
    },
  }}
>
  {children}
</Tooltip>