引言:
小白使用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*固定宽度的列 + 其余列的宽度
2. ant表格宽度使用百分比
- 如果使用
width属性设置宽度的话,只能使用具体的with值,不满足我的需求。 - 用元素包裹
Table,给元素设置width和padding,这样Table的宽度就是内容的宽度。 - 给
Table加classname,匹配到对应的单元格,设置每个列的width属性。所有单元格的宽度之和不能超过100%。(其实也可以某个列不设置width,这样Table会自动计算剩下的宽度)
3. 有时候会遇到表头长,对应单元格内容短,导致表头换行问题
- 可以给每个列的
width属性设置值,这样单元格内容的宽度就能撑开表头的宽度,这样就不会换行了。有时候也可以考虑在colums的render元素上加入如min-width,max-width、overflow等属性,让表格滚动。
#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.先看下表格是不是使用了filter,filter和ellipsis不能同时使用

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. 如何实现列合并(复杂表格的实现)
-
涉及的 api
onCell: 设置单元格属性,function(record, rowIndex)。通过返回一个包含 rowSpan 属性的对象,设置当前行需要合并的行数。- 表头只支持列合并,使用 column 里的 colSpan 进行设置。
- 表格支持行/列合并,使用 render 里的单元格属性 colSpan 或者 rowSpan 设值为 0 时,设置的表格不会渲染。
-
示例代码
// 二维原始数组
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
参考
-
antd官网: ant.design/components/…
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} />
参考
Tabs
1 基本使用方法
- 要使用的地方直接引入
- 每一个item用TabPane包裹
- 每个item要传入单独的key
场景
- 如果是要将list的每一项都成为TabPane,那么使用map就行
<Tab defaultActiveKey="1"> { list.map(item) => ( // tab:每个tab的名字 key:每个tab的key <TabPane tab="item.title" key="item.title"> // 列表下面的内容 <content /> </TabPane> ) } </Tab>
参考
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`: 展开了的 menu的 key, 用户看到的 menu 内容。`selectedkey`: 选中了的 key。通常配合 `selectable`属性一起使用,控制被选中时的状态


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>
```
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>
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>
)
}
```
参考
G2 chart
3. 一些 Q&A
1 二次挂载G2堆叠图的时候,打log判断有数据,但是chart绘制不出来
1.1 分析
堆叠图页面除了堆叠图还有其他图(称为堆叠图组件),堆叠图的数据和绘制只在组件挂载的时候初始化。之后父组件的重新渲染会触发堆叠图子组件的重新 render,但不会触发堆叠图的重新绘制。
打log 判断,关闭堆叠图组件->重新打开堆叠图组件 chart 的 view ID一直在递增,就是说在堆叠图组件卸载的时候 chart 并没有销毁。
1.2 尝试
- 在把 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()
})
- 如果图表数据在同一个界面会二次刷新,那么可以尝试下面的方法
- 在初始化图表时存储旧图表
const demo() => { // 存储为全局常量 const chart = null; // 存储在 state const [chart, setChart] = useState(null) }-
在有数据更新时,判断旧图表是否存在。如果存在,则调用chart.changeData(newData),只更新堆叠图的数据;如果不存在,则重新绘制图表。
Upload 组件
AntD 官网:ant.design/components/…
1. 一些 Q&A
1 点击上传按钮没有反应
1.1 分析
- 使用的 mac 电脑吗?
- 是:
-
使用 safari 浏览器打开网站,试试是否能正常上传。如果可以正常上传,那么可能是浏览器的文件访问权限没有打开。
-
权限开启方法:系统偏好设置-隐私-完全磁盘访问权限-解锁-勾选浏览器-重启浏览器
-
- 不是。试试 antd 官网的 upload 组件的上传按钮有反应吗?
- 有:检查在点击上传按钮时,控制台有没有报错,看下 upload 的使用姿势对了吗?onchange 等api 调用时是否存在 this 指向错误问题。
- 没有:那么再次检查浏览器上的文件访问权限问题
- 是: