AntD常用组件

1,608 阅读8分钟

引言:

小白使用AntD的组件使用方法、常用api、场景、避坑,欢迎指正~

Table

初识可视化表格,对仓库里封装的结构肥肠疑惑,照猫画虎把表格调整完后,记录一下使用历程。感叹里面的api课太多了,sorter、filter、fix、scroll、dataindex datasource、colunms...api 语义化很好了,但是还是会模糊他们的概念,且用且珍惜。

1 常用api

//表格列 colums -> item
{
  // 列头显示文字
  title: 'Address',
  // 列数据在数据项中对应的路径,支持通过数组查询嵌套路径
  dataIndex: 'address',
  key: 'address',
  // 表头的筛选菜单项
  filters: [
          { text: 'London', value: 'London' },
          { text: 'New York', value: 'New York' },
        ],
  filteredValue: filteredInfo.address || null,
  onFilter: (value, record) => record.address.includes(value),
  // 支持排序 
  sorter: (a, b) => a.address.length - b.address.length,
  sortOrder: sortedInfo.columnKey === 'address' && sortedInfo.order,
  // 超过宽度省略
  ellipsis: true,
  // 列宽度  
  width 
  // 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引,@return 里面可以设置表格行/列合并
  render:
},
  
// 数据数组 list
  
  
// Table  
colums={colums}
datasource={list}

2. Q & A

1. 组件表头和内容宽度不对齐

  • 如果是单独限制了内容的宽度,那么表头的宽度 》 内容限制的宽度,这样,表头和表格内容会错位
  • 表的宽度需要有一项是自由的
  • table的scroll -》 自由宽度列的宽 = scroll x - 2*固定宽度的列 + 其余列的宽度 image.png

2. ant表格宽度使用百分比

  • 如果使用width属性设置宽度的话,只能使用具体的with值,不满足我的需求。
  • 用元素包裹Table,给元素设置width和padding,这样Table的宽度就是内容的宽度。
  • Tableclassname,匹配到对应的单元格,设置每个列的width属性。所有单元格的宽度之和不能超过100%。(其实也可以某个列不设置width,这样Table会自动计算剩下的宽度)

3. 有时候会遇到表头长,对应单元格内容短,导致表头换行问题

  • 可以给每个列的width属性设置值,这样单元格内容的宽度就能撑开表头的宽度,这样就不会换行了。有时候也可以考虑在columsrender元素上加入如min-width,max-widthoverflow等属性,让表格滚动。
#js

const summaryCol = [
{
  title: intl("节点"),
  dataIndex: "operator",
  key: "operator",
  render: value => value
},

const myTabel = () => (
    <div style="width=60%">
        <div className="summary-table-wrap">
           <Table
              dataSource={summary}
              columns={summaryClolums}
              rowKey={(record, index) => index}
              pagination={false}
            />
        </div>
    </div>
)

#less
.summary-table-wrap {
    :global .ant-table-tbody {
      .ant-table-row {
        >td {
          width: 12%;
        }
      }
      td:first-child,
      td:last-child {
        width: 20%;
      }
    }
}

4. atd table ellispsis不生效

1.先看下表格是不是使用了filterfilter和ellipsis不能同时使用
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83ecf5aaf9924446ade0911b81945dd3~tplv-k3u1fbpfcp-watermark.image?)
2.如果要同时使用,那么可以给表格和表头设置width,给title单独设置overflow样式

5. 表头单元格内容超长,导致表头和表格单元错位,而且表头和表格会分开滚动。

1.设置Table的scroll-x,略微大于各个列的总和

2.设置列宽,百分比和固定宽度都行。不需要给每个列都设置宽度,找一列不设置宽度,antd会自动分配剩余一列的宽度

3.给每个title设置宽度。

4、5 问题代码示例

// js
/**
* titleWidth: 列的width除去左右padding的宽度,也就是纯内容的宽度
**/
const getTilte = (type, titleWidth) => {
  return (
    <span style={{
          maxWidth: `${headerWidth}px`,
          display: 'inline-block',
          overflow: 'hidden',
          textOverflow: 'ellipsis',
          lineHeight: 1
        }}
    >
      {type}
    </span>
  )
}
​
const columns = [
    {
      title: getTitle('functional_module', 66 * widthBase),
      dataIndex: 'functional_module',
      key: 'functional_module',
      // 包含padding的总宽度
      width: 88 * widthBase,
      fixed: 'left',
      render: (value, item, index) => (value)
},

6. Atd table key :Each child in a list should have a unique “key“ prop.

- rowkey:表格行 key 的取值,可以是字符串或一个函数
- key: 如果已经设置了唯一的 `dataIndex`,可以忽略这个属性,如果设置了rowkey,key可以不写
```
    <Table
        // 我一般写表格的名字
        key="myTable"
        dataSource={summary}
        columns={summaryClolums}
        // rowKey可以取每行数据中可以做唯一标识的字符串来做,
        rowKey={(record) => record.operator || '-'}
    />
```
  • 拓展小问题:数组list使用map时,如果每个 item 返回的是 dom 且没有 key 时,也会抱上面的警告,给外层的 dom 加 index 就行。
    values.map((item: any, idx: number) => {
         return (
           <p key={idx}>
               {item.dbtable}:{item.value}
           </p>
       );
     })

7. 其他情况的 key warning

参考:https://www.cnblogs.com/zhangyezi/p/13864188.html

8. 如何实现列合并(复杂表格的实现)

  1. 涉及的 api

    onCell: 设置单元格属性,function(record, rowIndex)。通过返回一个包含 rowSpan 属性的对象,设置当前行需要合并的行数。

    • 表头只支持列合并,使用 column 里的 colSpan 进行设置。
    • 表格支持行/列合并,使用 render 里的单元格属性 colSpan 或者 rowSpan 设值为 0 时,设置的表格不会渲染。
  2. 示例代码

// 二维原始数组
const originTableData = [
    {
      firstType: '电器',
      results: [
        {
          secondType: '电视',
          list: [
            {
              name: '小米',
              number: 20,
              price: 800
            },
            {
              name: '长虹',
              number: 10,
              price: 500
            }
          ]
        },
        {
          secondType: '冰箱',
          list: [
            {
              name: '美的',
              number: 20,
              price: 1000
            },
            {
              name: '海尔',
              number: 10,
              price: 888
            }
          ]
        }
      ]
    },
    {
      firstType: '食物',
      results: [
        {
          secondType: '零食',
          list: [
            {
              name: '坚果',
              number: 50,
              price: 8
            },
            {
              name: '辣条',
              number: 80,
              price: 3
            }
          ]
        },
        {
          secondType: '生疏',
          list: [
            {
              name: '青菜',
              number: 100,
              price: 1
            }
          ]
        }
      ]
    }
];

const columns = [
    {
        title: '一级分类',
        dataIndex: 'firstType',
        key: 'firstType',
        onCell: (record) => {
            const obj = {
                rowSpan: 0
            };
            // 第一列合并
            if (record.itemLength) {
                obj.rowSpan = record.itemLength;
            }
            return obj;
        },
    },
    {
        title: '二级分类',
        dataIndex: 'secondType',
        key: 'secondType',
        onCell: (record) => {
            const obj = {
                rowSpan: 0
            };
            // 第一列合并
            if (record.secondTypeLength) {
                obj.rowSpan = record.secondTypeLength;
            }
            return obj;
        },
    },
    {
        title: '三级分类',
        dataIndex: 'itemName',
        key: 'itemName',
    },
    {
        title: '数量',
        dataIndex: 'itemNumber',
        key: 'itemNumber',
    },
    {
        title: '单价',
        dataIndex: 'itemPrice',
        key: 'itemPrice',
    },
];

const App = () => {

    const tableData = formatColumns(originTableData);

    // 格式化数据,将数组扁平为带rowspan数的1维数组
    function formatColumns(lines) {
        const arr = [];
        // 将line数组转化为1维数组
        [...lines].map((firstTypeItem) => {
            firstTypeItem.results.map((secondTypeItem, secondTypeIndex) => {
                secondTypeItem.list.map((item, index) => {
                    const newItem = {
                        // 合并
                        firstType: firstTypeItem.firstType,
                        // 合并
                        secondType: secondTypeItem.secondType,
                        // 不合并
                        itemName: item.name,
                        itemNumber: item.number,
                        itemPrice: item.price
                    };
                    // 第一列合并的行数,为当前firstType下所有secondTypeItem的长度之和
                    // 依赖于firstType和secondType
                    if (index === 0 && secondTypeIndex === 0) {
                        newItem.itemLength = firstTypeItem.results.reduce((listA, listB) => {
                            return listA + listB.list.length;
                        }, 0);
                    }
                    // 第二列合并的行数,当前secondType下secondTypeItem的长度
                    // 依赖于secondType
                    if (index === 0) {
                        newItem.secondTypeLength = secondTypeItem.list.length;
                    }
                    arr.push(newItem);
                });
            });
        });
        // 1维数组加key,key值作为Table key
        arr.map((item, index) => {
            item.key = index;
            return item;
        });
        return arr;
    }

    return (
        <Table
            rowKey="key"
            bordered
            columns={columns}
            dataSource={tableData}
        />
    )
}

export default App

image.png

image.png

参考

  1. antd官网: ant.design/components/…

  2. www.cnblogs.com/syll/p/1051…

  3. segmentfault.com/q/101000001…

  4. blog.csdn.net/u010856177/…

  5. juejin.cn/post/684490…

  6. blog.csdn.net/halations/a…

  7. blog.csdn.net/jbj6568839z…

  8. blog.csdn.net/dream_enter…

  9. blog.csdn.net/lmyh1111/ar…

  10. blog.csdn.net/qq_45749061…

  11. www.cnblogs.com/steamed-twi…

  12. www.jianshu.com/p/c1cffad70…

Spin

1 loading效果

  • loading效果,用于页面和区块的加载中状态。
// 1 使用默认效果
<Spin spinning={this.state.loading} delay={500}>
  // 加载效果的显示与什么内容有关
  {container}
</Spin>
​
// 2 使用自定义icon
import { LoadingOutlined } from '@ant-design/icons';
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;
<Spin indicator={antIcon} />

参考

  1. ant.design/components/…

Tabs

1 基本使用方法

  • 要使用的地方直接引入
  • 每一个item用TabPane包裹
  • 每个item要传入单独的key

image.png

场景

  • 如果是要将list的每一项都成为TabPane,那么使用map就行
    <Tab defaultActiveKey="1">
    {
      list.map(item) => (
        // tab:每个tab的名字 key:每个tab的key
        <TabPane tab="item.title" key="item.title">
          // 列表下面的内容
          <content />
        </TabPane>
      )
    }
    </Tab>
    

参考

  1. ant.design/components/…

classnames

1 字符串拼接

<div 
    className={"bubble-box" +' '+styles['bubble-box']} 
 />

2 模板字符串拼接

<div
    className={`styles['bubble-box'] ${item.class === 'name' ? 'active' : ''}`}
/>

3 动态类名

3.1 传入变量

{classnames(className1, className2)}

<div 
  className={classnames(
      styles['bubble-box'], 
      styles[`${item.class}`], // 用模板字符串
      styles[item.class], // 直接使用变量也行
    )
  }
/>

3.2 为true的类名展示

{classnames(className1, {className2: true , className3: false})}

<div 
  className={classnames(
    styles['bubble-box'], { 
    [styles['empty']]: !value.length,
    [styles['disabled']]: disabled
  }
  )}
/>

Menu

1. 常用 api

  • Menu.SubMenu:子菜单,嵌套子菜单,包裹要展开的内容。
  • Menu.ItemGroup:菜单分组,将菜单 item 包裹在一个组里。严格意义上说不算是嵌套。
  • Menu.Item:菜单的 item
  • Selectable:当前选项是可以被选中的。默认值是 true
  • openkey:表示打开的是哪一级菜单
  • selectedkey:当前选中submenu 的 key,如果是多级菜单,那么就会是一个数组
  • onOpenChange:submenu 展开/收齐的回调。
  • mode:支持垂直、水平、和内嵌模式三种。vertical|horizontal|inline
  • onClick:点击子 menu 的时候调用,即使是卸载 menu 中,但也只有点击 MenuItem 的时候才会被调用

submenu

  • onTitleClick:获取到的是 submenu item 的 key。注意:submenu 没有 onClick api

2. 基本使用Demo

{/* 非受控的 Menu */}
<Menu
    style={{ width: 256 }}
    onClick={onClick}
    selectable={false}
>
    {/* sub 菜单 */}
    <SubMenu
        key="sub4"
        title="非受控的 Menu1"
    >
        {/* menu item */}
        <Menu.Item key="6">子菜单1</Menu.Item>
        <Menu.Item key="7">子菜单2</Menu.Item>
    </SubMenu>
</Menu>

3. 一些 Q&A

1. openkey 和 selectedkey 什么区别

 顾名思义,`openkey`: 展开了的 menukey, 用户看到的 menu 内容。`selectedkey`: 选中了的 key。通常配合 `selectable`属性一起使用,控制被选中时的状态

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4f35d03cbc3403c982b03b26da3d2b3~tplv-k3u1fbpfcp-watermark.image?)

![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54c63cd62f5e47cfadf3298bbf48083e~tplv-k3u1fbpfcp-watermark.image?)

2. onClick 方法可以写在 Menu.MenuItem上吗

**可以。** Menu 上的 onClick api就是作用在MenuItem上的,但写在 Menu 上就行,比较简洁。

3. key 和 keyPath 有什么区别

key:String,菜单当前层级的 key

keyPath: Array, 菜单所有层级的 key
```
    function onClick(e) {
        console.log('onClick', e)
    }
    
   {/* 3级 menu */}
    <Menu
        style={{ width: 256 }}
        onClick={onClick}
        selectable={false}
    >
        {/* sub 菜单 */}
        <SubMenu
            key="sub5"
            title="非受控的 Menu2"
        >
            {/* menu item */}
            <Menu.Item key="8">子菜单1</Menu.Item>
            <Menu.Item key="9">子菜单2</Menu.Item>
                        {/* sub 菜单 */}
            <SubMenu
                key="sub6"
                title="非受控的 Menu"
            >
                {/* menu item */}
                <Menu.Item key="10">子菜单1</Menu.Item>
                <Menu.Item key="11">子菜单2</Menu.Item>
            </SubMenu>
        </SubMenu>
    </Menu>
```

image.png

4. 多级菜单或嵌套菜单可以用 Menu 吗?

可以。 使用 SubMenu 包裹二级菜单即可,可以实现多级菜单。

5. MenuItem key 在不同的 SubMenu 目录下,可以使用同样的 key 吗?

不可以。 不管是否在一个SubMenu中,只要在一个组件中,key 就必须保证唯一,否则 key 会报警告

         {/* 2级 menu */}
        <Menu>
            {/* sub 菜单 */}
            <SubMenu
                key="sub1"
                title="受控的 Menu 1"
            >
                <Menu.Item key="1">子菜单1</Menu.Item>
                <Menu.Item key="2">子菜单2</Menu.Item>
            </SubMenu>
            {/* sub 菜单 */}
            <SubMenu
                key="sub2"
                title="受控的 Menu 2"
            >
                {/* menu item */}
                <Menu.Item key="1">子菜单1</Menu.Item>
                <Menu.Item key="2">子菜单2</Menu.Item>
            </SubMenu>
        </Menu>
        
         {/* 3级 menu */}
        <Menu>
            {/* sub 菜单 */}
            <SubMenu
                key="sub5"
                title="非受控的 Menu2"
            >
                {/* menu item */}
                <Menu.Item key="8">子菜单1</Menu.Item>
                <Menu.Item key="9">子菜单2</Menu.Item>
                {/* sub 菜单 */}
                <SubMenu
                    key="sub6"
                    title="非受控的 Menu"
                >
                    {/* menu item */}
                    <Menu.Item key="8">子菜单1</Menu.Item>
                    <Menu.Item key="9">子菜单2</Menu.Item>
                </SubMenu>
            </SubMenu>
        </Menu>

image.png

6. 点击了 MenuItem 之后菜单就关闭了,有方法可以让他在既定时机打开和关闭吗?

** 有。** submenu 包裹的 menuItem 是以popover 的形式弹出。如果要在点击 Menu.item 的时候做一些业务判断,那么需要将 menu 变为受控组件将 Menu 变为受控组件,设置 openkey 和 selectedkey

```
const CasterMenu = () => {
    const [openKeys, setOpenKeys] = useState([]);
    const [selectedKey, setSelectedKey] = useState([]);
    
    // 点击后关闭二级菜单
    // 点击 MenuItem,二级菜单
    // function({ item, key, keyPath, domEvent })
    function onClick(e) {
        console.log('onClick', e)
        setSelectedKey(e.keyPath);
        setOpenKeys([])
    }

    // 点击 subItem,一级菜单
    // function({ key, domEvent })
    function onTitleClick(e) {
        console.log('onTitleClick', e);
        setSelectedKey([e.key]);
        setOpenKeys([e.key])
    }
        function onConfirm() {
        console.log('onConfirm')
        setOpenKeys([]);
    }

    return (
        {/* 受控的 Menu */}
        {/* 点击 item 弹出 Popover */}
        <Menu
            style={{ width: 256 }}
            onClick={onClickShowPopoverMenu}
            selectable
            selectedKeys={selectedKey}
            openKeys={openKeys}
            defaultOpenKeys={[]}
            defaultSelectedKeys={[]}
        >
            {/* sub 菜单 */}
            <SubMenu
                key="sub3"
                title="受控的 Menu 弹出 Popconfirm"
                onTitleClick={onTitleClick}
            >
                <Popconfirm
                    title={'确定进行操作吗?'}
                    okText={'确定'}
                    cancelText={'取消'}
                    onConfirm={() => onConfirm()}
                >
                    <Menu.Item key="5">子菜单1</Menu.Item>
                </Popconfirm>
            </SubMenu>
        </Menu>
       )
   }
```

参考

  1. www.cnblogs.com/zpxm/p/1016…

  2. www.jianshu.com/p/698b855ed…

G2 chart

3. 一些 Q&A

1 二次挂载G2堆叠图的时候,打log判断有数据,但是chart绘制不出来

1.1 分析

堆叠图页面除了堆叠图还有其他图(称为堆叠图组件),堆叠图的数据和绘制只在组件挂载的时候初始化。之后父组件的重新渲染会触发堆叠图子组件的重新 render,但不会触发堆叠图的重新绘制。

打log 判断,关闭堆叠图组件->重新打开堆叠图组件 chart 的 view ID一直在递增,就是说在堆叠图组件卸载的时候 chart 并没有销毁。

1.2 尝试
  1. 在把 data写入 dataset之后,延迟1s 左右再绘制堆叠图
// 使用数据来自官网demo
// 初始化数据
initData() => {
    const data = [
      { State: 'WY', 小于5岁: 25635, '5至13岁': 1890, '14至17岁': 9314 },
      { State: 'DC', 小于5岁: 30352, '5至13岁': 20439, '14至17岁': 10225 },
      { State: 'VT', 小于5岁: 38253, '5至13岁': 42538, '14至17岁': 15757 },
      { State: 'ND', 小于5岁: 51896, '5至13岁': 67358, '14至17岁': 18794 },
      { State: 'AK', 小于5岁: 72083, '5至13岁': 85640, '14至17岁': 22153 },
    ];

    const ds = new DataSet();
    const dv = ds.createView().source(data);
    dv.transform({
      type: 'fold',
      fields: ['小于5岁', '5至13岁', '14至17岁'], // 展开字段集
      key: '年龄段', // key字段
      value: '人口数量', // value字段
      retains: ['State'], // 保留字段集,默认为除fields以外的所有字段
    });
    // 数据被加工成 {State: 'WY', 年龄段: '小于5岁', 人口数量: 25635}
}

// 绘制图表
initChart() => {
    const chart = new Chart({
      container: 'container',
      autoFit: true,
      height: 500,
    });
    chart.coordinate().transpose();
    chart.data(dv.rows);
    chart.scale('人口数量', { nice: true });
    chart.axis('State', {
      label: {
        offset: 12,
      },
    });
    chart.tooltip({
      shared: true,
      showMarkers: false,
    });
    chart
      .interval()
      .adjust('stack')
      .position('State*人口数量')
      .color('年龄段');
    chart.interaction('active-region');
    chart.render();
}


useEffect(() => {
        initData();
      	// 延迟绘制图表
        setTimeout(() => {
            initChart();
        }, 1000);
    }, []);

return <div id="container" />
1.3 其他场景
  • 在组件卸载的时候调用chart.destroy销毁图表,chart.clear清空绘制的内容
useEffect(() => {
    ...
    
    return () => chart && chart.destroy()
})

Upload 组件

AntD 官网:ant.design/components/…

1. 一些 Q&A

1 点击上传按钮没有反应

1.1 分析

  1. 使用的 mac 电脑吗?
    • 是:
      • 使用 safari 浏览器打开网站,试试是否能正常上传。如果可以正常上传,那么可能是浏览器的文件访问权限没有打开。

      • 权限开启方法:系统偏好设置-隐私-完全磁盘访问权限-解锁-勾选浏览器-重启浏览器

        image.png

        image.png

    • 不是。试试 antd 官网的 upload 组件的上传按钮有反应吗?
      • 有:检查在点击上传按钮时,控制台有没有报错,看下 upload 的使用姿势对了吗?onchange 等api 调用时是否存在 this 指向错误问题。
      • 没有:那么再次检查浏览器上的文件访问权限问题

官方文档

  1. api 文档:g2.antv.vision/zh/docs/api…
  2. G2官网:g2.antv.vision/zh/examples…