table-view
描述
A React table view component with column rendering, computed columns, formatting and sorting utilities
安装
npm i --save @kne/table-view
概述
@kne/table-view 是一个基于 React 和 Ant Design 的表格视图组件库,提供轻量灵活的 CSS Grid 表格布局,以及列渲染、格式化、排序、行选择等开箱即用的能力。专注于表格视图层与列渲染体系,可单独使用或嵌入业务页面。
核心组件
TableView
基于 CSS Grid 的表格视图组件,以栅格布局实现表头与表体的对齐展示。相比于 antd Table,它更轻量灵活,适合需要自定义渲染、移动端适配或卡片式表格场景。支持:
- 基于 24 栅格的列宽分配(
span属性)与固定宽度(width) - CSS Grid 自动布局,内容超出时自动撑开
- 行选择(checkbox 多选 / radio 单选)
- 树形数据(
dataType:list/tree/treeList),支持展开收起、层级缩进与全选含收起子行 - 行点击事件与自定义
render拆分表头/表体 - 通过
sortRender配合useSort实现表头排序
核心 Hooks
useSelectedRow
行选择 Hook,支持多选(checkbox)和单选(radio)两种模式。提供 getRowSelection(dataSource) 生成标准 rowSelection 配置,以及 selectedRowKeys、selectedRows、clearSelectedRows 等状态管理能力。
useSort
排序 Hook,配合 TableView 的 sortRender 实现表头排序交互。支持单列排序与多列排序,排序状态循环切换:DESC → ASC → 取消。提供 sortDataSource 工具函数用于本地排序(包含中文排序)。
渲染逻辑
桌面端:CSS Grid 表格
TableView 默认以 CSS Grid 渲染表头与表体。列配置经 resolveColumns 解析后,按 display !== false 过滤得到布局列,再计算 gridTemplateColumns(支持 span 栅格、width 固定宽、列宽拖动后的 colsSize)。每行数据走统一的单元格渲染管线:
computeColumnsValue:按列取原始值(getValueOf或dataSource[name]),经format格式化,过滤display为 false 或空值不展示的行computeDisplay:空值走placeholder/renderPlaceholder;非空值调用列render(见下方优先级)renderCellContent:按列ellipsis/cellFullWidth包裹Ellipsis与宽度约束后输出
自定义外层布局时,可通过 render={({ header, renderBody }) => ...} 拆分表头与表体;无数据时仍渲染表头并展示 empty。
列渲染优先级
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 | column.render |
自定义函数,最高优先级;存在时不再走 renderType 内置渲染 |
| 2 | renderType |
声明式类型(main、amount、tag、status、tagList、list、options、description 等),由 resolveColumn 注入对应 render 与列宽维度 |
| 3 | 原始值 | 无 render 且无有效 renderType 时,直接展示格式化后的 value |
render 与 renderType 可同时配置:renderType 仍负责注入 width / min / max / ellipsis 等布局维度,仅单元格内容由 render 覆盖。
renderType 支持尺寸修饰符组合,如 enum-small、main-large(short / small / large),维度可通过 globalParams.renderTypeSize 全局覆盖。
移动端:renderMobile
移动端判断统一使用 @kne/responsive-utils 的 useIsMobile()(断点 768px)。仅当 isRenderMobileActive(renderMobile, isMobile) 为 true 时启用移动端专用渲染,桌面端始终走 Grid 或 render。
renderMobile 值 |
行为 |
|---|---|
true |
默认卡片 List(MobileCardList):每行一张卡片,普通列纵向「标题 + 内容」,options 操作列固定右侧 |
function |
签名与 render 一致 ({ header, renderBody, ...props }) => ReactNode,完全接管移动端渲染,优先级高于 render;可调用 renderBody() 复用默认卡片 |
string |
从 preset({ renderMobile: { [name]: fn } }) 按名称查找;找到等同 function,未注册则视为未开启,移动端回退普通表格 |
未设置 / false / 未注册 string |
不开启移动端专用渲染,移动端仍显示 Grid 表格 |
默认卡片 List 细节:
- 普通列与
options列分离:isOptionsColumn识别操作列,字段列在卡片主体纵向排列,操作列在右侧独立区域 - 移动端操作区紧凑展示:
options列设置mobileOptions与buttonGroup.showLength: 0,仅保留「⋯」入口,避免按钮截断 - 支持
rowSelection(左侧 checkbox / radio);allowSelectedAll时列表顶部显示全选 - 卡片 padding 跟随
size,复用--kne-table-cell-paddingCSS 变量
列渲染类型系统
通过 renderType 属性,可以用声明式的方式定义列的渲染样式,无需手写 render 函数。内置 main、amount、tag、status、tagList、list、options、description 等类型,并支持与 short / small / large 尺寸修饰组合。
配合 format 属性可实现日期、金额、布尔值等展示格式化;配合 getValueOf 可传入 render 所需的复杂数据结构。
工具函数
| 导出项 | 说明 |
|---|---|
computeColumnsValue / computeDisplay |
列值计算与展示渲染 |
formatView / defaultFormat |
声明式值格式化 |
resolveColumns / resolveColumn |
列配置解析,自动注入 renderType 对应的 render 与宽度 |
preset / globalParams |
全局参数预设,定制 renderType 映射与标签颜色 |
getTagColor / renderTagItem / renderTagList |
Tag 渲染工具 |
getStatusType / renderStatusItem |
Status 渲染工具 |
buildTreeFromList / normalizeTreeData / flattenVisibleTree / flattenAllTree |
树形数据组装与展平 |
collectExpandableKeys / toggleExpandedKey / isTreeDataType |
树形展开状态工具 |
mergeTreeChildren / nodeCanExpand |
懒加载合并子节点 / 是否可展开 |
getTreeBreadcrumbItems |
移动端树形面包屑路径节点 |
toggleTreeCheck / getTreeCheckState / buildTreeKeyMaps |
树形勾选关联(checkRelation) |
使用场景
- 详情页信息展示:在 InfoPage 等场景中展示结构化数据列表
- 轻量列表页:不需要 antd Table 复杂能力时的简洁表格
- 移动端适配:
renderMobile启用卡片 List,或 CSS Grid 栅格式展示 - 自定义表格渲染:通过
render属性拆分表头与表体,灵活组合布局 - 列渲染复用:
renderType体系统一 tag、status、amount 等常见列样式
示例
示例代码
- TableView
- 表格视图组件,支持行选择、列宽设置、自定义渲染等
- _TableView(@kne/current-lib_table-view)[import * as _TableView from "@kne/table-view"],(@kne/current-lib_table-view/dist/index.css),antd(antd)
const { TableView } = _TableView;
const { Flex, Tag, Card, Button } = antd;
const { useState } = React;
const orderStatusMap = {
已完成: { type: 'success', text: '已完成' },
处理中: { type: 'processing', text: '处理中' },
待发货: { type: 'warning', text: '待发货' },
已取消: { type: 'default', text: '已取消' }
};
const dataSource = [
{
id: 'ORD20240115001',
customerName: '深圳市腾讯计算机系统有限公司',
contact: '张三',
phone: '13800138000',
amount: 42500,
status: '已完成',
orderDate: '2024-01-15',
deliveryDate: '2024-01-17'
},
{
id: 'ORD20240115002',
customerName: '华为技术有限公司',
contact: '李四',
phone: '13900149000',
amount: 85000,
status: '处理中',
orderDate: '2024-01-15',
deliveryDate: '2024-01-20'
},
{
id: 'ORD20240115003',
customerName: '阿里巴巴集团控股有限公司',
contact: '王五',
phone: '13700157000',
amount: 120000,
status: '待发货',
orderDate: '2024-01-14',
deliveryDate: '2024-01-22'
},
{
id: 'ORD20240115004',
customerName: '北京字节跳动科技有限公司',
contact: '赵六',
phone: '13600166000',
amount: 65000,
status: '已完成',
orderDate: '2024-01-13',
deliveryDate: '2024-01-16'
},
{
id: 'ORD20240115005',
customerName: '百度在线网络技术(北京)有限公司',
contact: '钱七',
phone: '13500175000',
amount: 95000,
status: '已取消',
orderDate: '2024-01-12',
deliveryDate: '',
disabled: true
}
];
const columns = [
{ name: 'id', title: '订单编号', width: 180, renderType: 'small' },
{ name: 'customerName', title: '客户名称', span: 10, renderType: 'main' },
{ name: 'contact', title: '联系人', width: 80 },
{ name: 'phone', title: '联系电话', width: '130px', render: value => value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3') },
{
name: 'amount',
title: '订单金额(元)',
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{ name: 'orderDate', title: '下单日期', format: 'date' },
{ name: 'deliveryDate', title: '预计送达', format: 'date' },
{
name: 'status',
title: '订单状态',
width: 100,
renderType: 'status',
getValueOf: item => orderStatusMap[item.status] || { type: 'default', text: item.status }
}
];
const WithCheckbox = () => {
const [selectKeys, setSelectKeys] = useState([]);
const totalAmount = selectKeys.reduce((sum, id) => sum + (dataSource.find(d => d.id === id)?.amount || 0), 0);
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>
多选 + 全选 — 最后一行「已取消」为 <code>disabled</code>,全选勾选态与选中结果都会跳过该行
</div>
<Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
<span>
已选 <strong>{selectKeys.length}</strong> 个订单,总金额 <strong style={{ color: '#52c41a' }}>¥{totalAmount.toLocaleString()}</strong>
</span>
</Flex>
<TableView
dataSource={dataSource}
columns={columns}
rowSelection={{
type: 'checkbox',
allowSelectedAll: true,
selectedRowKeys: selectKeys,
onChange: setSelectKeys
}}
/>
</div>
);
};
const WithRadio = () => {
const [selectKeys, setSelectKeys] = useState([]);
const selectedOrder = dataSource.find(d => d.id === selectKeys[0]);
const radioColumns = [
{ name: 'id', title: '订单编号', width: 160, renderType: 'small' },
{ name: 'customerName', title: '客户名称', renderType: 'main' },
{
name: 'amount',
title: '订单金额(元)',
width: 120,
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{
name: 'status',
title: '订单状态',
width: 100,
renderType: 'status',
getValueOf: item => orderStatusMap[item.status] || { type: 'default', text: item.status }
}
];
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>单选 — 左侧为 Radio 选择列</div>
<Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
<span>已选订单:{selectedOrder ? `${selectedOrder.id} (${selectedOrder.customerName})` : '无'}</span>
{selectedOrder && <Tag color="blue">¥{selectedOrder.amount.toLocaleString()}</Tag>}
</Flex>
<TableView
dataSource={dataSource}
columns={radioColumns}
rowSelection={{
type: 'radio',
selectedRowKeys: selectKeys,
onChange: setSelectKeys
}}
/>
</div>
);
};
const WithCustomRender = () => {
const displayData = dataSource.slice(0, 3);
const totalAmount = displayData.reduce((sum, item) => sum + item.amount, 0);
return (
<Card
size="small"
title="近期订单"
extra={
<Button type="link" size="small" onClick={() => console.log('查看全部')}>
查看全部
</Button>
}
styles={{ body: { padding: 0 } }}
>
<Flex
justify="space-between"
align="center"
style={{ padding: '12px 16px', background: '#fafafa', borderBottom: '1px solid #f0f0f0' }}
>
<Flex gap={8} align="center">
<Tag color="blue">{displayData.length} 笔</Tag>
<span style={{ color: 'rgba(0,0,0,0.65)', fontSize: 13 }}>
合计 <strong style={{ color: '#52c41a' }}>¥{totalAmount.toLocaleString()}</strong>
</span>
</Flex>
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: 12 }}>render 自定义外层布局</span>
</Flex>
<TableView
dataSource={displayData}
columns={columns}
render={({ renderBody }) => (
<div style={{ overflowX: 'auto' }}>{renderBody(displayData)}</div>
)}
/>
</Card>
);
};
const WithEmpty = () => (
<TableView
dataSource={[]}
columns={columns}
empty={
<div style={{ padding: 24, color: '#999' }}>
暂无订单数据
</div>
}
/>
);
const BaseExample = () => {
return (
<Flex vertical gap={24}>
<div style={{ background: '#f5f5f5', padding: '12px', borderRadius: '8px' }}>
订单列表 - 共 <strong>{dataSource.length}</strong> 个订单
</div>
<TableView dataSource={dataSource} columns={columns} />
<WithCheckbox />
<WithRadio />
<WithCustomRender />
<WithEmpty />
</Flex>
);
};
render(<BaseExample />);
- tree
- 树状数据:dataType 为 tree / treeList,支持展开收起、懒加载、移动端默认/自定义卡片
- _TableView(@kne/current-lib_table-view)[import * as _TableView from "@kne/table-view"],(@kne/current-lib_table-view/dist/index.css),antd(antd)
const { TableView, mergeTreeChildren } = _TableView;
const { Space, Button, Radio, Checkbox, Flex } = antd;
const { useState } = React;
const columns = [
{ name: 'name', title: '名称', renderType: 'main' },
{ name: 'code', title: '编码', width: 120 },
{ name: 'owner', title: '负责人', width: 100 }
];
const treeData = [
{
id: '1',
name: '华东区',
code: 'EAST',
owner: '张三',
children: [
{
id: '1-1',
name: '上海',
code: 'SH',
owner: '李四',
children: [
{ id: '1-1-1', name: '浦东分部', code: 'SH-PD', owner: '王五' },
{ id: '1-1-2', name: '徐汇分部', code: 'SH-XH', owner: '赵六' }
]
},
{ id: '1-2', name: '杭州', code: 'HZ', owner: '钱七' }
]
},
{
id: '2',
name: '华北区',
code: 'NORTH',
owner: '孙八',
children: [{ id: '2-1', name: '北京', code: 'BJ', owner: '周九' }]
}
];
const treeListData = [
{ id: '1', name: '华东区', code: 'EAST', owner: '张三', parentId: null },
{ id: '1-1', name: '上海', code: 'SH', owner: '李四', parentId: '1' },
{ id: '1-1-1', name: '浦东分部', code: 'SH-PD', owner: '王五', parentId: '1-1' },
{ id: '1-1-2', name: '徐汇分部', code: 'SH-XH', owner: '赵六', parentId: '1-1' },
{ id: '1-2', name: '杭州', code: 'HZ', owner: '钱七', parentId: '1' },
{ id: '2', name: '华北区', code: 'NORTH', owner: '孙八', parentId: '' },
{ id: '2-1', name: '北京', code: 'BJ', owner: '周九', parentId: '2' }
];
const lazyRootData = [
{ id: 'org-1', name: '集团总部', code: 'HQ', owner: '张三', parentId: null, hasChildren: true },
{ id: 'org-2', name: '分公司', code: 'BR', owner: '李四', parentId: null, hasChildren: true }
];
const lazyChildrenMap = {
'org-1': [
{ id: 'org-1-1', name: '研发中心', code: 'RD', owner: '王五', hasChildren: true },
{ id: 'org-1-2', name: '市场部', code: 'MKT', owner: '赵六', hasChildren: false }
],
'org-1-1': [
{ id: 'org-1-1-1', name: '前端组', code: 'FE', owner: '钱七', hasChildren: false },
{ id: 'org-1-1-2', name: '后端组', code: 'BE', owner: '孙八', hasChildren: false }
],
'org-2': [{ id: 'org-2-1', name: '华南办', code: 'SC', owner: '周九', hasChildren: false }]
};
const OrgTreeMobileCard = ({ item, meta, breadcrumb, checked, indeterminate, disabled, onChange, onExpand, loading }) => {
const path = (breadcrumb || []).slice(0, -1);
const hasChildren = !!meta?.hasChildren;
const level = meta?.level || 0;
const isChild = level > 0;
return (
<div
style={{
background: checked ? 'var(--primary-color-1, #e6f4ff)' : isChild ? 'var(--bg-color-grey-1, #f5f5f5)' : '#fff',
border: `1px solid ${checked ? 'var(--primary-color-2, #91caff)' : isChild ? '#e8e8e8' : '#f0f0f0'}`,
borderLeft: `${isChild ? level * 18 : 1}px solid ${checked ? 'var(--primary-color, #1677ff)' : isChild ? '#bfbfbf' : '#f0f0f0'}`,
borderRadius: 8,
padding: '12px 14px',
color: checked ? 'var(--primary-color, #1677ff)' : undefined,
transition: 'background 200ms, border-color 200ms, border-left-width 200ms'
}}
>
<Flex
align="center"
gap={8}
style={{
paddingBottom: 10,
marginBottom: 10,
borderBottom: `1px solid ${isChild ? '#ebebeb' : '#f0f0f0'}`
}}
>
<button
type="button"
aria-label={meta?.expanded ? '收起' : '展开'}
disabled={!hasChildren || loading}
onClick={e => {
e.stopPropagation();
if (hasChildren) {
onExpand?.();
}
}}
style={{
width: 22,
height: 22,
border: 'none',
background: 'transparent',
padding: 0,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
cursor: hasChildren ? 'pointer' : 'default',
color: hasChildren ? 'rgba(0,0,0,0.45)' : 'transparent',
transform: meta?.expanded ? 'rotate(90deg)' : 'none',
transition: 'transform 200ms',
flexShrink: 0,
fontSize: 12,
lineHeight: 1,
opacity: loading ? 0.5 : 1
}}
>
{loading ? '…' : '▸'}
</button>
<Checkbox checked={checked} indeterminate={indeterminate} disabled={disabled} onChange={onChange} onClick={e => e.stopPropagation()} />
<span
style={{
flexShrink: 0,
fontSize: 11,
lineHeight: '18px',
padding: '0 6px',
borderRadius: 4,
background: isChild ? 'rgba(0,0,0,0.06)' : 'var(--primary-color-1, #e6f4ff)',
color: isChild ? 'rgba(0,0,0,0.45)' : 'var(--primary-color, #1677ff)'
}}
>
{isChild ? `子级 L${level}` : '根节点'}
</span>
<div
style={{
flex: 1,
minWidth: 0,
fontSize: 12,
lineHeight: '20px',
color: 'rgba(0,0,0,0.45)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{path.length > 0 ? path.join(' / ') : '—'}
</div>
</Flex>
<div style={{ fontSize: isChild ? 14 : 16, fontWeight: isChild ? 500 : 600, lineHeight: 1.4, color: checked ? 'inherit' : 'rgba(0,0,0,0.88)' }}>{item.name}</div>
<Flex gap={16} style={{ marginTop: 8, fontSize: 13, lineHeight: 1.5, color: checked ? 'inherit' : 'rgba(0,0,0,0.45)' }}>
<span>编码 {item.code}</span>
<span>负责人 {item.owner}</span>
</Flex>
</div>
);
};
const TreeExample = () => {
const { selectedRowKeys, getRowSelection, clearSelectedRows } = TableView.useSelectedRow({ rowKey: 'id' });
const mobileSelection = TableView.useSelectedRow({ rowKey: 'id' });
const customMobileSelection = TableView.useSelectedRow({ rowKey: 'id' });
const [expandedKeys, setExpandedKeys] = useState(false);
const [checkRelation, setCheckRelation] = useState('parent');
const [lazyData, setLazyData] = useState(lazyRootData);
const handleLoadChildren = (item, { key }) =>
new Promise(resolve => {
setTimeout(() => {
const children = lazyChildrenMap[key] || [];
setLazyData(prev =>
mergeTreeChildren(prev, children, {
parentKeyValue: key,
dataType: 'treeList',
rowKey: 'id',
parentKey: 'parentId',
hasChildrenKey: 'hasChildren'
})
);
resolve();
}, 800);
});
return (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<div style={{ marginBottom: 8 }}>tree:嵌套 children</div>
<TableView dataSource={treeData} columns={columns} dataType="tree" defaultExpandedKeys />
</div>
<div>
<div style={{ marginBottom: 8 }}>懒加载:hasChildren + onLoadChildren + mergeTreeChildren(展开显示 loading)</div>
<TableView dataSource={lazyData} columns={columns} dataType="treeList" onLoadChildren={handleLoadChildren} />
</div>
<div>
<div style={{ marginBottom: 8 }}>移动端树形默认卡片:三角 + 勾选 + 面包屑</div>
<TableView
dataSource={treeListData}
columns={columns}
dataType="treeList"
defaultExpandedKeys
treeTitleKey="name"
renderMobile
rowSelection={mobileSelection.getRowSelection(treeListData, { allowSelectedAll: true, checkRelation: 'parent' })}
/>
</div>
<div>
<div style={{ marginBottom: 8 }}>自定义 renderMobile:根节点 / 子级(meta.level)视觉区分</div>
<TableView
dataSource={treeListData}
columns={columns}
dataType="treeList"
defaultExpandedKeys
treeTitleKey="name"
rowSelection={customMobileSelection.getRowSelection(treeListData, { allowSelectedAll: true, checkRelation: 'parent' })}
renderMobile={({ displayDataSource, renderToolbar, getRowKey, getSelectionProps, getTreeRowMeta, getBreadcrumb, isExpandLoading, onToggleExpand }) => (
<div style={{ background: '#f5f5f5', borderRadius: 8, padding: 12 }}>
<div style={{ marginBottom: 12 }}>{renderToolbar()}</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{(displayDataSource || []).map(item => {
const key = getRowKey(item);
const selection = getSelectionProps(item);
return (
<OrgTreeMobileCard
key={key}
item={item}
meta={getTreeRowMeta(item)}
breadcrumb={getBreadcrumb(item)}
loading={isExpandLoading(item)}
onExpand={() => onToggleExpand(key)}
{...selection}
/>
);
})}
</div>
</div>
)}
/>
</div>
<div>
<div style={{ marginBottom: 8 }}>checkRelation:parent(默认,值只留父级)/ all(含子孙)/ independent(互不影响)</div>
<Space style={{ marginBottom: 8 }} wrap>
<Radio.Group
value={checkRelation}
optionType="button"
options={[
{ label: 'parent', value: 'parent' },
{ label: 'all', value: 'all' },
{ label: 'independent', value: 'independent' }
]}
onChange={e => {
setCheckRelation(e.target.value);
clearSelectedRows();
}}
/>
</Space>
<TableView dataSource={treeListData} columns={columns} dataType="treeList" defaultExpandedKeys rowSelection={getRowSelection(treeListData, { allowSelectedAll: true, checkRelation })} />
<div style={{ marginTop: 8 }}>已选 key:{selectedRowKeys.join(', ') || '无'}</div>
</div>
<div>
<div style={{ marginBottom: 8 }}>受控展开:true 全开 / false 全关 / key 数组</div>
<Space style={{ marginBottom: 8 }}>
<Button size="small" onClick={() => setExpandedKeys(true)}>
全部展开
</Button>
<Button size="small" onClick={() => setExpandedKeys(false)}>
全部收起
</Button>
<Button size="small" onClick={() => setExpandedKeys(['1', '1-1'])}>
展开指定节点
</Button>
</Space>
<TableView dataSource={treeData} columns={columns} dataType="tree" expandedKeys={expandedKeys} onExpandedKeysChange={setExpandedKeys} />
</div>
</Space>
);
};
render(<TreeExample />);
- renderType
- 列 renderType 配置:main / amount / tag / status / tagList / list / options / description,支持与 short / small / large 尺寸修饰组合
- _TableView(@kne/current-lib_table-view)[import * as _TableView from "@kne/table-view"],(@kne/current-lib_table-view/dist/index.css),antd(antd)
const { TableView } = _TableView;
const { Flex } = antd;
const statusMap = {
待发货: { type: 'warning', text: '待发货' },
处理中: { type: 'processing', text: '处理中' },
已完成: { type: 'success', text: '已完成' }
};
const categoryMap = {
企业客户: { type: 'default', text: '企业客户' },
战略客户: { type: 'processing', text: '战略客户' }
};
const dataSource = [
{
id: 'ORD001',
customerName: '深圳市腾讯计算机系统有限公司',
category: '企业客户',
tags: ['物流', '加急'],
keywords: ['合同', '附件', '春节前'],
remark: '客户要求春节前完成交付,需协调物流加急处理,并同步更新合同附件。',
amount: 42500,
status: '待发货'
},
{
id: 'ORD002',
customerName: '华为技术有限公司',
category: '战略客户',
tags: ['评审', '配置清单'],
keywords: ['需求评审', '配置清单'],
remark: '项目处于需求评审阶段,待客户确认最终配置清单后安排发货。',
amount: 85000,
status: '处理中'
},
{
id: 'ORD003',
customerName: '阿里巴巴集团控股有限公司',
category: '企业客户',
tags: ['拣货', '付款完成'],
keywords: ['付款', '拣货', '发货'],
remark: '已完成付款,仓库正在拣货,预计两个工作日内发出第一批货物。',
amount: 120000,
status: '已完成'
}
];
const columns = [
{ name: 'id', title: '编号', renderType: 'small' },
{
name: 'customerName',
title: '客户名称',
renderType: 'main',
primary: true,
hover: true,
onClick: ({ item }) => console.log('查看客户详情:', item)
},
{
name: 'category',
title: '分类',
renderType: 'tag-short',
getValueOf: item => categoryMap[item.category]
},
{
name: 'tags',
title: '标签',
renderType: 'tagList',
getValueOf: item =>
(item.tags || []).map(text => ({
type: text === '加急' ? 'error' : 'processing',
text
}))
},
{
name: 'keywords',
title: '关键词',
renderType: 'list',
split: '、',
getValueOf: item => item.keywords
},
{ name: 'remark', title: '备注', renderType: 'description' },
{
name: 'amount',
title: '金额',
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{
name: 'status',
title: '状态',
renderType: 'status',
getValueOf: item => statusMap[item.status]
},
{
name: 'options',
title: '操作',
renderType: 'options',
getValueOf: item => {
const actions = [
{ children: '查看', onClick: () => console.log('查看', item.id) },
{ children: '编辑', onClick: () => console.log('编辑', item.id) }
];
if (item.status !== '已完成') {
actions.push({
children: '删除',
isDelete: true,
message: `确定删除 ${item.id} 吗?`,
onClick: () => console.log('删除', item.id)
});
}
return actions;
}
}
];
const BaseExample = () => {
return (
<Flex vertical gap={24}>
<div style={{ color: '#666', fontSize: 13, lineHeight: 1.8 }}>
<p>
列配置 <code>renderType</code> 声明列的渲染方式,无需手写 <code>render</code>。内置类型:
</p>
<ul style={{ margin: '8px 0', paddingLeft: 20 }}>
<li><code>main</code> — 主信息列,支持 <code>primary</code> / <code>hover</code> / <code>onClick</code></li>
<li><code>amount</code> — 金额列,右对齐,配合 <code>format</code> 格式化</li>
<li><code>tag</code> — 单个 Tag,<code>getValueOf</code> 返回 <code>{'{ type, text }'}</code></li>
<li><code>status</code> — 状态 Badge,<code>getValueOf</code> 返回 <code>{'{ type, text }'}</code></li>
<li><code>tagList</code> — 多个 Tag 列表</li>
<li><code>list</code> — 文本列表,可用 <code>split</code> 指定分隔符</li>
<li><code>options</code> — 操作列,<code>getValueOf</code> 返回按钮配置数组</li>
<li><code>description</code> — 长文本描述列</li>
</ul>
<p>
可与尺寸修饰词组合:<code>short</code> / <code>small</code> / <code>large</code>(如 <code>tag-short</code>、<code>status-small</code>、<code>main-large</code>)。
通过 <code>getValueOf</code> 传入 render 所需数据结构,通过 <code>format</code> 做日期、金额等展示格式化。
</p>
</div>
<TableView dataSource={dataSource} columns={columns} />
</Flex>
);
};
render(<BaseExample />);
- column render
- 列同时配置 render 与 renderType 时,render 优先级最高,覆盖内置 renderType 的单元格渲染
- _TableView(@kne/current-lib_table-view)[import * as _TableView from "@kne/table-view"],(@kne/current-lib_table-view/dist/index.css),antd(antd)
const { TableView } = _TableView;
const { Flex, Tag } = antd;
const statusMap = {
待发货: { type: 'warning', text: '待发货' },
处理中: { type: 'processing', text: '处理中' },
已完成: { type: 'success', text: '已完成' }
};
const dataSource = [
{
id: 'ORD001',
customerName: '深圳市腾讯计算机系统有限公司',
amount: 42500,
status: '待发货'
},
{
id: 'ORD002',
customerName: '华为技术有限公司',
amount: 85000,
status: '处理中'
},
{
id: 'ORD003',
customerName: '阿里巴巴集团控股有限公司',
amount: 120000,
status: '已完成'
}
];
const columns = [
{ name: 'id', title: '编号', renderType: 'small' },
{ name: 'customerName', title: '客户名称', renderType: 'main' },
{
name: 'amount',
title: '金额',
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{
name: 'status',
title: '状态(仅 renderType)',
renderType: 'status',
getValueOf: item => statusMap[item.status]
},
{
name: 'statusRender',
title: '状态(render 优先)',
renderType: 'status',
getValueOf: item => statusMap[item.status],
render: (value, { dataSource }) => (
<span style={{ color: '#1677ff' }}>
自定义渲染:{dataSource.status}(未走 status Badge)
</span>
)
}
];
const BaseExample = () => {
return (
<Flex vertical gap={24}>
<div style={{ color: '#666', fontSize: 13, lineHeight: 1.8 }}>
<p>
列同时配置 <code>render</code> 与 <code>renderType</code> 时,
<Tag color="blue" style={{ margin: '0 4px' }}>render 优先级最高</Tag>
,会直接使用自定义 <code>render</code>,不再走内置 renderType。
</p>
<ul style={{ margin: '8px 0', paddingLeft: 20 }}>
<li>「状态(仅 renderType)」列:走内置 <code>status</code>,渲染 Badge</li>
<li>「状态(render 优先)」列:同样写了 <code>renderType: 'status'</code>,但因存在 <code>render</code>,最终显示自定义内容</li>
<li>renderType 仍可提供列宽等维度(width / min / max),仅单元格内容渲染被 <code>render</code> 覆盖</li>
</ul>
</div>
<TableView dataSource={dataSource} columns={columns} />
</Flex>
);
};
render(<BaseExample />);
- renderMobile
- 移动端专用渲染:true 为默认卡片 List;function 完全接管;string 从 preset 按名称查找;支持 mobileSortToolbar 排序工具栏
- _TableView(@kne/current-lib_table-view)[import * as _TableView from "@kne/table-view"],(@kne/current-lib_table-view/dist/index.css),antd(antd)
const { TableView, preset } = _TableView;
const { Flex, Tag, Card, Button, Dropdown, Checkbox, Radio } = antd;
const { useState, useMemo } = React;
const statusMap = {
已完成: { color: 'success', text: '已完成' },
处理中: { color: 'processing', text: '处理中' },
待发货: { color: 'warning', text: '待发货' }
};
const dataSource = [
{
id: 'ORD001',
customerName: '深圳市腾讯计算机系统有限公司',
contact: '张三',
phone: '13800138000',
amount: 42500,
status: '已完成'
},
{
id: 'ORD002',
customerName: '华为技术有限公司',
contact: '李四',
phone: '13900149000',
amount: 85000,
status: '处理中'
},
{
id: 'ORD003',
customerName: '阿里巴巴集团控股有限公司',
contact: '王五',
phone: '13700157000',
amount: 120000,
status: '待发货'
}
];
const columns = [
{ name: 'id', title: '订单编号', width: 120, renderType: 'small' },
{ name: 'customerName', title: '客户名称', span: 10, renderType: 'main', sort: true },
{ name: 'contact', title: '联系人', width: 80 },
{ name: 'phone', title: '联系电话', width: 130, render: value => value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3') },
{
name: 'amount',
title: '订单金额',
sort: true,
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{
name: 'status',
title: '状态',
width: 100,
renderType: 'status',
getValueOf: item => ({ type: statusMap[item.status]?.color || 'default', text: item.status })
},
{
name: 'options',
title: '操作',
renderType: 'options',
getValueOf: item => [
{ children: '查看', onClick: () => console.log('查看', item.id) },
{ children: '编辑', onClick: () => console.log('编辑', item.id) },
{ children: '删除', isDelete: true, message: `确定删除 ${item.id} 吗?`, onClick: () => console.log('删除', item.id) }
]
}
];
preset({
renderMobile: {
orderCard: ({ renderBody, dataSource = [] }) => {
const totalAmount = dataSource.reduce((sum, item) => sum + item.amount, 0);
return (
<div
className="preset-order-card-example"
style={{
borderRadius: 12,
background: '#f5f7fa',
padding: 16
}}
>
<style>{`
.preset-order-card-example .info-page-table-mobile-card:not(.is-mobile-card-selected):not(.is-mobile-card-selected-all) {
background: linear-gradient(135deg, #ffffff 0%, #f9f0ff 52%, #eef2ff 100%) !important;
border-color: #e8dfff !important;
}
.preset-order-card-example .info-page-table-mobile-card:not(.is-mobile-card-selected):not(.is-mobile-card-selected-all):hover {
background: linear-gradient(135deg, #fafafa 0%, #f3ebff 52%, #e8eeff 100%) !important;
}
`}</style>
<div style={{ marginBottom: 16 }}>
<Flex justify="space-between" align="center" gap={8} style={{ marginBottom: 4 }}>
<div style={{ fontSize: 17, fontWeight: 600, color: 'rgba(0,0,0,0.88)' }}>近期订单</div>
<Tag color="purple" style={{ margin: 0, flexShrink: 0 }}>
preset: orderCard
</Tag>
</Flex>
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>
{dataSource.length} 笔 · 合计 ¥{totalAmount.toLocaleString()}
</div>
</div>
<div
className="info-page-table"
style={{
'--kne-table-cell-padding': '14px 8px'
}}
>
{renderBody()}
</div>
</div>
);
}
}
});
const formatPhone = phone => phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
const getOrderActions = item => [
{ key: 'view', label: '查看', onClick: () => console.log('查看', item.id) },
{ key: 'edit', label: '编辑', onClick: () => console.log('编辑', item.id) },
{ key: 'delete', label: '删除', danger: true, onClick: () => console.log('删除', item.id) }
];
const OrderMobileCard = ({ item, checked, disabled, onCheckChange, selectionType = 'checkbox' }) => {
const status = statusMap[item.status] || { color: 'default', text: item.status };
const actionItems = getOrderActions(item);
const isSelected = checked;
const SelectionControl = selectionType === 'radio' ? Radio : Checkbox;
return (
<div
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 12,
background: isSelected ? 'var(--primary-color-1, #e6f4ff)' : '#fff',
borderRadius: 12,
padding: 16,
border: `1px solid ${isSelected ? 'var(--primary-color-2, var(--primary-color, #1677ff))' : 'transparent'}`,
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.04)',
color: isSelected ? 'var(--primary-color, #1677ff)' : undefined,
boxSizing: 'border-box'
}}
>
<SelectionControl checked={checked} disabled={disabled} onChange={onCheckChange} style={{ marginTop: 2, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<Flex justify="space-between" align="center" gap={8} style={{ marginBottom: 10 }}>
<Flex align="center" gap={8} wrap="wrap" style={{ flex: 1, minWidth: 0 }}>
<Tag color={status.color} style={{ margin: 0 }}>
{status.text}
</Tag>
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>{item.id}</span>
</Flex>
<Dropdown
trigger={['click']}
menu={{
items: actionItems.map(({ key, label, danger, onClick }) => ({
key,
label,
danger,
onClick: ({ domEvent }) => {
domEvent.stopPropagation();
onClick();
}
}))
}}
>
<Button type="text" size="small" style={{ padding: '0 4px' }} onClick={e => e.stopPropagation()}>
···
</Button>
</Dropdown>
</Flex>
<div
style={{
fontSize: 16,
fontWeight: 600,
color: 'rgba(0,0,0,0.88)',
lineHeight: 1.5,
marginBottom: 6
}}
>
{item.customerName}
</div>
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', lineHeight: 1.6 }}>
{item.contact} · {formatPhone(item.phone)}
</div>
<Flex
justify="space-between"
align="center"
gap={12}
style={{
marginTop: 14,
paddingTop: 12,
borderTop: '1px solid #f0f0f0'
}}
>
<Flex align="baseline" gap={6} style={{ flex: 1, minWidth: 0 }}>
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', flexShrink: 0 }}>订单金额</span>
<span style={{ fontSize: 16, fontWeight: 600, color: '#1677ff' }}>¥{item.amount.toLocaleString()}</span>
</Flex>
<Flex gap={4} align="center" style={{ flexShrink: 0 }}>
{actionItems.slice(0, 2).map(({ key, label, onClick }) => (
<Button
key={key}
type="link"
size="small"
style={{ padding: '0 4px', height: 'auto' }}
onClick={e => {
e.stopPropagation();
onClick();
}}
>
{label}
</Button>
))}
</Flex>
</Flex>
</div>
</div>
);
};
const DefaultMobileCards = () => {
const [selectKeys, setSelectKeys] = useState([]);
const totalAmount = selectKeys.reduce((sum, id) => sum + (dataSource.find(d => d.id === id)?.amount || 0), 0);
return (
<div>
<div style={{ marginBottom: 12, color: '#666', fontSize: 13, lineHeight: 1.7 }}>
<code>renderMobile={'{true}'}</code>:移动端启用默认卡片 List(每行一张卡片,操作列靠右);
开启 <code>allowSelectedAll</code> 后顶部工具栏左侧显示全选。请用示例预览的手机模式查看效果。
</div>
<Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
<span>
已选 <strong>{selectKeys.length}</strong> 个订单,总金额 <strong style={{ color: '#52c41a' }}>¥{totalAmount.toLocaleString()}</strong>
</span>
</Flex>
<TableView
dataSource={dataSource}
columns={columns}
size="large"
renderMobile
rowSelection={{
type: 'checkbox',
allowSelectedAll: true,
selectedRowKeys: selectKeys,
onChange: keys => setSelectKeys(keys)
}}
/>
</div>
);
};
const SortState = ({ sort }) => (
<div style={{ marginBottom: 12, padding: '10px 12px', background: '#f5f5f5', borderRadius: 8, fontSize: 13 }}>
当前排序:
{sort.length ? (
<span>
{sort.map(item => (
<Tag key={item.name} color="blue" style={{ marginLeft: 8 }}>
{item.name} {item.sort}
</Tag>
))}
</span>
) : (
<span style={{ marginLeft: 8, color: '#999' }}>无</span>
)}
</div>
);
const MobileSortExample = () => {
const { sort, sortRender, mobileSortToolbar } = TableView.useSort({
defaultSort: [{ name: 'amount', sort: 'DESC' }],
onSortChange: value => console.log('移动端排序变更:', value)
});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
return (
<Flex vertical gap={24}>
<div>
<div style={{ marginBottom: 12, color: '#666', fontSize: 13, lineHeight: 1.7 }}>
移动端排序:列配置 <code>sort: true</code>,配合 <code>TableView.useSort</code> 传入 <code>mobileSortToolbar</code>。
工具栏居右,下拉选择排序列,方向为单图标,点击按「降序 → 升序 → 取消」循环(与 PC 端列头排序一致);也可在下拉选「取消排序」清除。数据需自行用 <code>sortDataSource</code> 排序。
</div>
<SortState sort={sort} />
<TableView
dataSource={sortedData}
columns={columns}
size="large"
renderMobile
sortRender={sortRender}
mobileSortToolbar={mobileSortToolbar}
/>
</div>
<div>
<div style={{ marginBottom: 12, color: '#666', fontSize: 13, lineHeight: 1.7 }}>
排序与全选同时开启:工具栏左侧全选、右侧排序。
</div>
<MobileSortWithSelectAll />
</div>
</Flex>
);
};
const MobileSortWithSelectAll = () => {
const [selectKeys, setSelectKeys] = useState([]);
const { sort, sortRender, mobileSortToolbar } = TableView.useSort({});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
return (
<TableView
dataSource={sortedData}
columns={columns}
size="large"
renderMobile
sortRender={sortRender}
mobileSortToolbar={mobileSortToolbar}
rowSelection={{
type: 'checkbox',
allowSelectedAll: true,
selectedRowKeys: selectKeys,
onChange: keys => setSelectKeys(keys)
}}
/>
);
};
const CustomMobileRender = () => {
const [selectKeys, setSelectKeys] = useState([]);
const { sort, sortRender, mobileSortToolbar } = TableView.useSort({});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
const totalAmount = dataSource.reduce((sum, item) => sum + item.amount, 0);
const selectedAmount = selectKeys.reduce((sum, id) => sum + (dataSource.find(d => d.id === id)?.amount || 0), 0);
return (
<div>
<div style={{ marginBottom: 12, color: '#666', fontSize: 13, lineHeight: 1.7 }}>
<code>renderMobile</code> 为 function 时完全接管渲染,可自定义卡片内容;
全选 / 排序请用回调里的 <code>renderToolbar()</code>(与默认 MobileCard 同一套实现),
行勾选用 <code>getSelectionProps(item)</code>,不必自己维护全选状态或排序 UI。
桌面端仍走 <code>render</code>。
</div>
<Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
<span>
已选 <strong>{selectKeys.length}</strong> 个订单,金额 <strong style={{ color: '#52c41a' }}>¥{selectedAmount.toLocaleString()}</strong>
</span>
</Flex>
<Card size="small" title="近期订单" extra={<Tag>桌面 render</Tag>} styles={{ body: { padding: 0 } }}>
<Flex
justify="space-between"
align="center"
style={{ padding: '12px 16px', background: '#fafafa', borderBottom: '1px solid #f0f0f0' }}
>
<Flex gap={8} align="center">
<Tag color="blue">{dataSource.length} 笔</Tag>
<span style={{ color: 'rgba(0,0,0,0.65)', fontSize: 13 }}>
合计 <strong style={{ color: '#52c41a' }}>¥{totalAmount.toLocaleString()}</strong>
</span>
</Flex>
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: 12 }}>桌面端 render 自定义外层</span>
</Flex>
<TableView
dataSource={sortedData}
columns={columns}
sortRender={sortRender}
mobileSortToolbar={mobileSortToolbar}
rowSelection={{
type: 'checkbox',
allowSelectedAll: true,
selectedRowKeys: selectKeys,
onChange: keys => setSelectKeys(keys)
}}
render={({ renderBody }) => <div style={{ overflowX: 'auto' }}>{renderBody()}</div>}
renderMobile={({ dataSource: mobileList = [], renderToolbar, getSelectionProps, getRowKey }) => (
<div
style={{
borderRadius: 12,
background: '#f5f7fa',
padding: 16
}}
>
<div style={{ marginBottom: 16 }}>
<Flex justify="space-between" align="center" gap={8} style={{ marginBottom: 4 }}>
<div style={{ fontSize: 17, fontWeight: 600, color: 'rgba(0,0,0,0.88)' }}>近期订单</div>
<Tag color="processing" style={{ margin: 0, flexShrink: 0 }}>
renderMobile
</Tag>
</Flex>
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>
{mobileList.length} 笔 · 合计 ¥{mobileList.reduce((sum, item) => sum + item.amount, 0).toLocaleString()}
</div>
</div>
{renderToolbar()}
<Flex vertical gap={12} style={{ marginTop: 12 }}>
{mobileList.map(item => {
const selection = getSelectionProps(item);
return (
<OrderMobileCard
key={getRowKey(item)}
item={item}
checked={selection.checked}
disabled={selection.disabled}
onCheckChange={selection.onChange}
/>
);
})}
</Flex>
</div>
)}
/>
</Card>
</div>
);
};
const CustomMobileRadioRender = () => {
const [selectKeys, setSelectKeys] = useState([]);
const { sort, sortRender, mobileSortToolbar } = TableView.useSort({});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
const selectedOrder = dataSource.find(item => item.id === selectKeys[0]);
return (
<div>
<div style={{ marginBottom: 12, color: '#666', fontSize: 13, lineHeight: 1.7 }}>
自定义 <code>renderMobile</code> 单选:<code>rowSelection.type</code> 设为 <code>radio</code>,
卡片上的 Radio 直接绑 <code>getSelectionProps(item)</code>,选中态与切换逻辑由 TableView 管理;
工具栏 <code>renderToolbar()</code> 此时仅显示排序(单选无全选)。
</div>
<Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
<span>
当前选中:
{selectedOrder ? (
<strong>
{selectedOrder.id} · ¥{selectedOrder.amount.toLocaleString()}
</strong>
) : (
<span style={{ color: '#999' }}>未选择</span>
)}
</span>
</Flex>
<TableView
dataSource={sortedData}
columns={columns}
sortRender={sortRender}
mobileSortToolbar={mobileSortToolbar}
rowSelection={{
type: 'radio',
selectedRowKeys: selectKeys,
onChange: keys => setSelectKeys(keys)
}}
renderMobile={({ dataSource: mobileList = [], renderToolbar, getSelectionProps, getRowKey }) => (
<div
style={{
borderRadius: 12,
background: '#f5f7fa',
padding: 16
}}
>
{renderToolbar()}
<Flex vertical gap={12} style={{ marginTop: 12 }}>
{mobileList.map(item => {
const selection = getSelectionProps(item);
return (
<OrderMobileCard
key={getRowKey(item)}
item={item}
selectionType="radio"
checked={selection.checked}
disabled={selection.disabled}
onCheckChange={selection.onChange}
/>
);
})}
</Flex>
</div>
)}
/>
</div>
);
};
const PresetStringRender = () => {
const [selectKeys, setSelectKeys] = useState([]);
const { sort, sortRender, mobileSortToolbar } = TableView.useSort({
defaultSort: [{ name: 'amount', sort: 'DESC' }]
});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
return (
<Flex vertical gap={24}>
<div>
<div style={{ marginBottom: 12, color: '#666', fontSize: 13, lineHeight: 1.7 }}>
<code>renderMobile="orderCard"</code>:通过 <code>preset({'{ renderMobile }'})</code> 注册名称对应的渲染函数;
仅移动端生效,支持全选与选中样式。可配合 <code>mobileSortToolbar</code> 开启排序。
</div>
<TableView
dataSource={sortedData}
columns={columns}
size="large"
renderMobile="orderCard"
sortRender={sortRender}
mobileSortToolbar={mobileSortToolbar}
rowSelection={{
type: 'checkbox',
allowSelectedAll: true,
selectedRowKeys: selectKeys,
onChange: keys => setSelectKeys(keys)
}}
/>
</div>
<div>
<div style={{ marginBottom: 12, color: '#666', fontSize: 13, lineHeight: 1.7 }}>
<code>renderMobile="notRegistered"</code>:preset 中未注册时视为未开启,移动端仍显示普通表格。
</div>
<TableView dataSource={dataSource} columns={columns} renderMobile="notRegistered" />
</div>
</Flex>
);
};
const BaseExample = () => {
return (
<Flex vertical gap={32}>
<DefaultMobileCards />
<MobileSortExample />
<CustomMobileRender />
<CustomMobileRadioRender />
<PresetStringRender />
</Flex>
);
};
render(<BaseExample />);
- useSelectedRow
- 行选择 Hook,配合 TableView 实现多选、全选、批量操作与单选
- _TableView(@kne/current-lib_table-view)[import * as _TableView from "@kne/table-view"],(@kne/current-lib_table-view/dist/index.css),antd(antd)
const { TableView } = _TableView;
const { Button, Flex, Space, message } = antd;
const orderStatusMap = {
已完成: { type: 'success', text: '已完成' },
处理中: { type: 'processing', text: '处理中' },
待发货: { type: 'warning', text: '待发货' },
已取消: { type: 'default', text: '已取消' }
};
const dataSource = [
{
id: 'ORD20240115001',
customerName: '深圳市腾讯计算机系统有限公司',
contact: '张三',
amount: 42500,
status: '待发货',
orderDate: '2024-01-15'
},
{
id: 'ORD20240115002',
customerName: '华为技术有限公司',
contact: '李四',
amount: 85000,
status: '处理中',
orderDate: '2024-01-15'
},
{
id: 'ORD20240115003',
customerName: '阿里巴巴集团控股有限公司',
contact: '王五',
amount: 120000,
status: '待发货',
orderDate: '2024-01-14'
},
{
id: 'ORD20240115004',
customerName: '北京字节跳动科技有限公司',
contact: '赵六',
amount: 65000,
status: '已完成',
orderDate: '2024-01-13'
},
{
id: 'ORD20240115005',
customerName: '百度在线网络技术(北京)有限公司',
contact: '钱七',
amount: 95000,
status: '已取消',
orderDate: '2024-01-12'
}
];
const columns = [
{ name: 'id', title: '订单编号', width: 180, renderType: 'small' },
{ name: 'customerName', title: '客户名称', width: 220, renderType: 'main' },
{ name: 'contact', title: '联系人', width: 100 },
{
name: 'amount',
title: '订单金额(元)',
width: 130,
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{ name: 'orderDate', title: '下单日期', width: 120, format: 'date' },
{
name: 'status',
title: '订单状态',
width: 100,
renderType: 'status',
getValueOf: item => orderStatusMap[item.status] || { type: 'default', text: item.status }
}
];
const BatchToolbar = ({ selectedRowKeys, selectedRows, clearSelectedRows, onBatchShip, onBatchExport }) => {
const totalAmount = selectedRows.reduce((sum, item) => sum + (item.amount || 0), 0);
return (
<Flex justify="space-between" align="center" style={{ marginBottom: 12, padding: '12px', background: '#f5f5f5', borderRadius: 8 }}>
<Space>
<span>
已选 <strong>{selectedRowKeys.length}</strong> 个订单,总金额 <strong style={{ color: '#52c41a' }}>¥{totalAmount.toLocaleString()}</strong>
</span>
<Button type="primary" size="small" disabled={!selectedRowKeys.length} onClick={onBatchShip}>
批量发货
</Button>
<Button size="small" disabled={!selectedRowKeys.length} onClick={onBatchExport}>
批量导出
</Button>
<Button size="small" disabled={!selectedRowKeys.length} onClick={clearSelectedRows}>
清空选择
</Button>
</Space>
</Flex>
);
};
const CheckboxExample = () => {
const { selectedRowKeys, selectedRows, getRowSelection, clearSelectedRows } = TableView.useSelectedRow({ rowKey: 'id' });
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>多选模式 useSelectedRow + getRowSelection</div>
<BatchToolbar
selectedRowKeys={selectedRowKeys}
selectedRows={selectedRows}
clearSelectedRows={clearSelectedRows}
onBatchShip={() => {
message.success(`已批量发货 ${selectedRowKeys.length} 个订单`);
clearSelectedRows();
}}
onBatchExport={() => message.info(`正在导出 ${selectedRowKeys.length} 个订单`)}
/>
<TableView dataSource={dataSource} columns={columns} rowSelection={getRowSelection(dataSource)} />
</div>
);
};
const radioColumns = [
{ name: 'id', title: '订单编号', width: 160, renderType: 'small' },
{ name: 'customerName', title: '客户名称', renderType: 'main' },
{
name: 'amount',
title: '订单金额(元)',
width: 120,
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{
name: 'status',
title: '订单状态',
width: 100,
renderType: 'status',
getValueOf: item => orderStatusMap[item.status] || { type: 'default', text: item.status }
}
];
const RadioExample = () => {
const { selectedRowKeys, selectedRows, getRowSelection } = TableView.useSelectedRow({ rowKey: 'id', type: 'radio' });
const selectedOrder = selectedRows[0];
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>
单选模式 <code>type: 'radio'</code> — 选择列在表格最左侧
</div>
<div style={{ marginBottom: 12, padding: '12px', background: '#f5f5f5', borderRadius: 8 }}>
当前选中:{selectedOrder ? `${selectedOrder.id}(${selectedOrder.customerName})` : '无'}
</div>
<TableView dataSource={dataSource} columns={radioColumns} rowSelection={getRowSelection(dataSource)} />
</div>
);
};
const BaseExample = () => {
return (
<Flex vertical gap={24}>
<CheckboxExample />
<RadioExample />
</Flex>
);
};
render(<BaseExample />);
- useSort
- 排序 Hook,配合 TableView 实现表头排序、单列/多列排序与 sortDataSource 本地排序
- _TableView(@kne/current-lib_table-view)[import * as _TableView from "@kne/table-view"],(@kne/current-lib_table-view/dist/index.css),antd(antd)
const { TableView } = _TableView;
const { Flex, Tag } = antd;
const { useMemo } = React;
const orderStatusMap = {
已完成: { type: 'success', text: '已完成' },
处理中: { type: 'processing', text: '处理中' },
待发货: { type: 'warning', text: '待发货' },
已取消: { type: 'default', text: '已取消' }
};
const dataSource = [
{ id: 'ORD001', customerName: '深圳市腾讯计算机系统有限公司', amount: 42500, status: '已完成', orderDate: '2024-01-15' },
{ id: 'ORD002', customerName: '华为技术有限公司', amount: 85000, status: '处理中', orderDate: '2024-01-14' },
{ id: 'ORD003', customerName: '阿里巴巴集团控股有限公司', amount: 120000, status: '待发货', orderDate: '2024-01-16' },
{ id: 'ORD004', customerName: '北京字节跳动科技有限公司', amount: 65000, status: '已完成', orderDate: '2024-01-13' },
{ id: 'ORD005', customerName: '百度在线网络技术(北京)有限公司', amount: 95000, status: '已取消', orderDate: '2024-01-12' }
];
const columns = [
{ name: 'id', title: '订单编号', width: 140, sort: { single: true }, renderType: 'small' },
{ name: 'customerName', title: '客户名称', width: 240, sort: true, renderType: 'main' },
{
name: 'amount',
title: '订单金额(元)',
width: 130,
sort: true,
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{ name: 'orderDate', title: '下单日期', width: 120, sort: true, format: 'date' },
{
name: 'status',
title: '订单状态',
width: 100,
renderType: 'status',
getValueOf: item => orderStatusMap[item.status] || { type: 'default', text: item.status }
}
];
const SortState = ({ sort }) => (
<div style={{ marginBottom: 12, padding: '12px', background: '#f5f5f5', borderRadius: 8 }}>
当前排序:
{sort.length ? (
<span>
{sort.map(item => (
<Tag key={item.name} color="blue" style={{ marginLeft: 8 }}>
{item.name} {item.sort}
</Tag>
))}
</span>
) : (
<span style={{ marginLeft: 8, color: '#999' }}>无</span>
)}
</div>
);
const SingleSortExample = () => {
const { sort, sortRender, mobileSortToolbar } = TableView.useSort({
onSortChange: value => console.log('单列排序变更:', value)
});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>单列排序(订单编号 sort: {'{ single: true }'})</div>
<SortState sort={sort} />
<TableView dataSource={sortedData} columns={columns} sortRender={sortRender} mobileSortToolbar={mobileSortToolbar} renderMobile />
</div>
);
};
const MultiSortExample = () => {
const { sort, sortRender, mobileSortToolbar } = TableView.useSort({
defaultSort: [{ name: 'orderDate', sort: 'DESC' }],
onSortChange: value => console.log('多列排序变更:', value)
});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>多列排序(默认按下单日期降序,金额/日期支持多列排序)</div>
<SortState sort={sort} />
<TableView dataSource={sortedData} columns={columns} sortRender={sortRender} mobileSortToolbar={mobileSortToolbar} renderMobile />
</div>
);
};
const BaseExample = () => {
return (
<Flex vertical gap={24}>
<div style={{ color: '#666', fontSize: 13 }}>
列配置 <code>sort: true</code> 开启排序,<code>sort: {'{ single: true }'}</code> 为单列排序。点击表头三角切换 DESC → ASC → 取消。
</div>
<SingleSortExample />
<MultiSortExample />
</Flex>
);
};
render(<BaseExample />);
- column ellipsis
- 表头 title 超出列宽自动省略、悬停 tooltip;单元格 ellipsis 配置基于 antd Typography 实现内容省略
- _TableView(@kne/current-lib_table-view)[import * as _TableView from "@kne/table-view"],(@kne/current-lib_table-view/dist/index.css),antd(antd)
const { TableView } = _TableView;
const { Flex, Tag } = antd;
const { useMemo } = React;
const orderStatusMap = {
已完成: { type: 'success', text: '已完成' },
处理中: { type: 'processing', text: '处理中' },
待发货: { type: 'warning', text: '待发货' }
};
const dataSource = [
{
id: 'ORD001',
customerName: '深圳市腾讯计算机系统有限公司深圳总部研发中心',
remark: '客户要求春节前完成交付,需协调物流加急处理,并同步更新合同附件与验收标准说明文档。',
amount: 42500,
status: '待发货'
},
{
id: 'ORD002',
customerName: '华为技术有限公司坂田基地采购中心',
remark: '项目处于需求评审阶段,待客户确认最终配置清单后安排发货。',
amount: 85000,
status: '处理中'
},
{
id: 'ORD003',
customerName: '阿里巴巴集团控股有限公司滨江园区',
remark: '已完成付款,仓库正在拣货,预计两个工作日内发出第一批货物。',
amount: 120000,
status: '待发货'
}
];
const columns = [
{ name: 'id', title: '订单编号(系统流水号)', width: 110, renderType: 'small' },
{
name: 'customerName',
title: '客户名称(签约主体全称)',
width: 140,
renderType: 'main',
ellipsis: true
},
{
name: 'remark',
title: '备注说明(内部流转备注)',
width: 160,
renderType: 'description',
ellipsis: { showTitle: true }
},
{
name: 'amount',
title: '订单应付金额(含税,单位:元)',
width: 120,
sort: true,
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{
name: 'status',
title: '订单履约状态(业务状态)',
width: 100,
renderType: 'status',
getValueOf: item => orderStatusMap[item.status] || { type: 'default', text: item.status }
}
];
const TIP_TAG_STYLE = { marginRight: 8 };
const Tips = () => (
<div style={{ color: '#666', fontSize: 13, lineHeight: 1.8 }}>
<div>
<Tag style={TIP_TAG_STYLE} color="blue">表头省略</Tag>
列 <code>title</code> 超出列宽时自动单行省略,悬停 tooltip 显示完整标题;带排序的列同样生效,无需额外配置。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="green">单元格省略</Tag>
列配置 <code>ellipsis: true</code> 或 <code>ellipsis: {'{ showTitle: true }'}</code>,单元格内容超出时省略,悬停显示完整内容(基于 antd Typography)。
</div>
<div style={{ color: '#999' }}>
本示例刻意使用较长表头与较窄列宽,便于观察省略与 tooltip 效果;可将鼠标悬停在表头或单元格上查看。
</div>
</div>
);
const BaseExample = () => {
const { sort, sortRender } = TableView.useSort({});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
return (
<Flex vertical gap={24}>
<Tips />
<TableView dataSource={sortedData} columns={columns} sortRender={sortRender} />
</Flex>
);
};
render(<BaseExample />);
- size
- 单元格 padding 尺寸:默认 8px,small 为 4px,large 为 14px 8px;支持 CSS 变量 --kne-table-cell-padding-* 覆盖
- _TableView(@kne/current-lib_table-view)[import * as _TableView from "@kne/table-view"],(@kne/current-lib_table-view/dist/index.css),antd(antd)
const { TableView } = _TableView;
const { Flex, Radio } = antd;
const { useState } = React;
const dataSource = [
{
id: 'ORD001',
customerName: '深圳市腾讯计算机系统有限公司',
contact: '张三',
amount: 42500,
status: '已完成'
},
{
id: 'ORD002',
customerName: '华为技术有限公司',
contact: '李四',
amount: 85000,
status: '处理中'
},
{
id: 'ORD003',
customerName: '阿里巴巴集团控股有限公司',
contact: '王五',
amount: 120000,
status: '待发货'
}
];
const columns = [
{ name: 'id', title: '订单编号', width: 120, renderType: 'small' },
{ name: 'customerName', title: '客户名称', span: 10, renderType: 'main' },
{ name: 'contact', title: '联系人', width: 80 },
{
name: 'amount',
title: '订单金额',
width: 120,
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{ name: 'status', title: '状态', width: 100 }
];
const SizeDemo = ({ title, description, size }) => (
<div>
<div style={{ marginBottom: 8 }}>
<strong>{title}</strong>
<span style={{ marginLeft: 8, color: '#666', fontSize: 13 }}>{description}</span>
</div>
<TableView dataSource={dataSource} columns={columns} size={size} />
</div>
);
const InteractiveSize = () => {
const [size, setSize] = useState('default');
return (
<div>
<Flex align="center" gap={12} style={{ marginBottom: 12 }}>
<strong>切换 size</strong>
<Radio.Group
optionType="button"
value={size}
onChange={e => setSize(e.target.value)}
options={[
{ label: 'default (8px)', value: 'default' },
{ label: 'small (4px)', value: 'small' },
{ label: 'large (14px 8px)', value: 'large' }
]}
/>
</Flex>
<TableView dataSource={dataSource} columns={columns} size={size === 'default' ? undefined : size} />
</div>
);
};
const BaseExample = () => {
return (
<Flex vertical gap={24}>
<div style={{ background: '#f5f5f5', padding: '12px', borderRadius: 8, fontSize: 13 }}>
<div>
<code>size</code> 控制单元格 padding:默认 <code>8px</code>,<code>small</code> 为 <code>4px</code>,
<code>large</code> 为 <code>14px 8px</code>
</div>
<div style={{ marginTop: 4, color: '#666' }}>
可通过 CSS 变量覆盖:
<code>--kne-table-cell-padding-default</code> /
<code>--kne-table-cell-padding-small</code> /
<code>--kne-table-cell-padding-large</code>,或直接设
<code>--kne-table-cell-padding</code>
</div>
</div>
<InteractiveSize />
<SizeDemo title="default" description="padding: 8px" />
<SizeDemo title='size="small"' description="padding: 4px" size="small" />
<SizeDemo title='size="large"' description="padding: 14px 8px" size="large" />
<div>
<div style={{ marginBottom: 8 }}>
<strong>CSS 变量覆盖</strong>
<span style={{ marginLeft: 8, color: '#666', fontSize: 13 }}>
--kne-table-cell-padding-default: 12px 16px
</span>
</div>
<div style={{ '--kne-table-cell-padding-default': '12px 16px' }}>
<TableView dataSource={dataSource} columns={columns} />
</div>
</div>
</Flex>
);
};
render(<BaseExample />);
API
TableView
表格视图组件,基于 CSS Grid 布局实现,支持列配置、行选择、排序、自定义渲染等能力。
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| dataSource | array | - | 表格数据源 |
| columns | array | - | 列配置,见下方 columns 说明 |
| rowKey | string | function | 'id' |
行唯一标识字段名或取值函数 |
| rowSelection | object | - | 行选择配置,见下方 rowSelection 说明 |
| placeholder | string | '-' |
空值占位符 |
| emptyIsPlaceholder | boolean | true |
空值是否显示占位符 |
| valueIsEmpty | function | @kne/is-empty |
判断值是否为空的函数 |
| empty | ReactNode | <Empty /> |
无数据时的展示内容 |
| headerStyle | object | - | 表头自定义样式,仅在 render 自定义渲染时作用于 header |
| onRowSelect | function | - | 行点击回调 (item, { columns, dataSource }) => void |
| render | function | - | 自定义渲染 ({ header, renderBody }) => ReactNode,可拆分表头与表体;返回值会包在默认 .info-page-table 容器内,单元格 padding 与普通 TableView 一致 |
| renderMobile | boolean | function | string | - | 仅移动端生效。true 使用默认卡片 List;为 function 时完全接管渲染(见下方回调参数);为 string 时从 preset({ renderMobile }) 按名称取渲染函数,未注册则视为未开启 |
| sortRender | function | - | 排序按钮渲染,由 useSort 提供(桌面端表头) |
| mobileSortToolbar | function | - | 移动端排序工具栏,由 useSort 提供;与 sortRender 配合传入 TableView,由 renderToolbar / 默认卡片复用 |
| context | object | - | 列渲染上下文,会传入 render、getValueOf 等回调 |
| className | string | - | 自定义类名 |
| size | 'small' | 'large' |
- | 单元格内边距:默认 8px,small 为 4px,large 为 14px 8px;可通过 CSS 变量覆盖,见下方说明 |
| dataType | 'list' | 'tree' | 'treeList' |
'list' |
数据形态。list 为扁平列表;tree 使用 childrenKey 嵌套子节点;treeList 按 parentKey 组装为树(空父级为根) |
| parentKey | string | 'parentId' |
treeList 模式下父子关联字段 |
| childrenKey | string | 'children' |
tree / 组装后的子节点字段名 |
| hasChildrenKey | string | 'hasChildren' |
节点是否仍有子级的标记字段;为 true 时即使尚无 children 也显示展开三角 |
| treeTitleKey | string | function | 'name' |
移动端树形面包屑文案字段;为 function 时签名 (item) => string |
| onLoadChildren | function | - | 懒加载:(item, { key }) => void | Promise;首次展开且本地无子节点时触发,组件不改 dataSource,请用 mergeTreeChildren 合并 |
| expandedKeys | true | false | Array |
- | 受控展开。true 全部展开,false 全部收起,数组为展开的行 key |
| defaultExpandedKeys | true | false | Array |
false |
非受控初始展开状态 |
| onExpandedKeysChange | function | - | 展开变化回调 (keys) => void;单节点切换时为 key 数组,全开/全关可为 true / false |
| indentSize | number | 16 |
树形每层缩进宽度(px) |
树形模式说明:
- 首列在勾选/单选前显示展开三角;无
rowSelection时三角占原勾选位置 - 子级行(
level > 0)默认灰色背景 - 全选范围包含全部节点(含收起的子行);表体仅渲染当前展开可见行
- 移动端默认卡片同样支持缩进与展开三角,全选基于全部节点
- 懒加载:
hasChildren === true且无本地子节点时,展开会调用onLoadChildren,三角显示 loading;同一节点只自动请求一次 - 移动端树形卡片顶部:
展开三角+checkbox/radio(若有)+面包屑(根到当前节点路径,按treeTitleKey取值,/分隔)
renderMobile 为 function 时,TableView 会传入已接好 rowSelection / mobileSortToolbar 的能力,自定义布局只需选用,不必自己实现全选或排序:
| 回调参数 | 说明 |
|---|---|
dataSource |
原始数据源 |
displayDataSource |
树形下为当前展开可见行;非树形与 dataSource 相同 |
columns |
布局后的列配置 |
rowKey / rowSelection / context / empty |
与 TableView 一致 |
isTree |
是否为树形模式 |
renderBody |
渲染默认移动端卡片 List(含顶部工具栏) |
renderToolbar |
渲染组件级工具栏(全选居左、排序居右);可自由决定摆放位置 |
getRowKey(item) |
按 rowKey 取行 key |
getSelectionProps(item) |
返回 { checked, indeterminate, disabled, onChange } |
getTreeRowMeta(item) |
树形行 meta:{ key, level, hasChildren, expanded } |
getBreadcrumb(item) |
面包屑文案数组(根 → 当前) |
isExpandLoading(item) |
该行是否正在懒加载子节点 |
onToggleExpand(key) |
切换展开 |
onSelectionChange |
行选择切换,签名与内部逻辑一致 |
renderMobile={({ displayDataSource, renderToolbar, getSelectionProps, getRowKey, getTreeRowMeta, getBreadcrumb, onToggleExpand }) => (
<>
{renderToolbar()}
{displayDataSource.map(item => {
const selection = getSelectionProps(item);
const meta = getTreeRowMeta(item);
return <MyTreeCard key={getRowKey(item)} item={item} meta={meta} breadcrumb={getBreadcrumb(item)} onExpand={() => onToggleExpand(getRowKey(item))} {...selection} />;
})}
</>
)}
renderMobile 默认卡片 List 行为:
- 每行一张卡片,卡片间距
12px,表格外边框隐藏 - 卡片 padding 跟随
size(复用--kne-table-cell-padding) - 普通列以「标题 + 内容」纵向排列;
options操作列固定在卡片右侧(ButtonGroup) - 支持
rowSelection(左侧 checkbox / radio);开启allowSelectedAll或排序(传入mobileSortToolbar)时,卡片列表顶部显示工具栏:全选居左、排序居右(下拉选择排序列,方向为单图标,点击按「降序 → 升序 → 取消」循环,与 PC 端列头排序一致;也可在下拉选「取消排序」清除) - 树形(
dataType为tree/treeList)时卡片顶部为:展开三角+勾选(若有)+面包屑(根到当前,按treeTitleKey,/分隔) - 为 string 时通过
preset({ renderMobile: { [name]: renderFn } })注册,用法:renderMobile="orderCard"
字符串类型说明:
| 值 | 行为 |
|---|---|
true |
默认卡片 List |
function |
自定义渲染,完全接管 |
string |
从 preset 查找同名渲染函数;找到则等同 function,未找到则移动端回退普通表格 |
false / 未注册 string |
不开启移动端专用渲染 |
单元格 padding 由 CSS 变量控制,可在外层覆盖:
.info-page-table {
--kne-table-cell-padding-default: 8px; /* 默认 size */
--kne-table-cell-padding-small: 4px; /* size="small" */
--kne-table-cell-padding-large: 14px 8px; /* size="large" */
/* 或直接覆盖当前生效值:--kne-table-cell-padding: 10px; */
}
columns
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| name | string | - | 字段名,对应 dataSource 中的属性 |
| title | ReactNode | - | 列标题 |
| width | number | string | - | 列最小宽度,支持数字(如 180,视为 180px)或字符串(如 '180px'),内容超出时会自动撑开 |
| span | number | - | 列占比(基于 24 栅格),未设置时自动均分剩余栅格 |
| min | number | - | 列最小宽度(px),由 renderType 自动注入,也可手动覆盖 |
| max | number | - | 列最大宽度(px),由 renderType 自动注入,也可手动覆盖 |
| align | string | 'top' |
垂直对齐方式 |
| justify | string | 'flex-start' |
水平对齐方式 |
| format | string | function | - | 值格式化,见下方 format 说明 |
| render | function | - | 自定义单元格渲染 (value, { column, dataSource, context }) => ReactNode;与 renderType 同时存在时优先级最高 |
| renderType | string | - | 声明式列渲染类型,见下方 renderType 说明;存在 render 时仅保留列宽等维度,不参与单元格渲染 |
| getValueOf | function | - | 自定义取值 (dataSource, { column, context }) => any,用于 render 所需复杂数据 |
| sort | boolean | object | - | 是否支持排序,{ single: true } 为单列排序 |
| ellipsis | boolean | object | false |
超出省略,基于 antd Typography;true 开启省略与 tooltip,{ showTitle: false } 关闭 tooltip |
| display | boolean | function | - | 是否显示该列 |
| emptyIsPlaceholder | boolean | - | 该列空值是否显示占位符 |
| placeholder | string | - | 该列空值占位符 |
| renderPlaceholder | function | - | 自定义空值渲染 |
| type | string | - | 列类型,如 'options' 表示操作列 |
| primary | boolean | - | main 类型:是否显示主色样式 |
| hover | boolean | - | main 类型:是否显示悬停样式 |
| onClick | function | - | main 类型:点击回调 ({ item, colItem, event }) => void |
| split | string | - | list 类型:列表项分隔符,默认 , |
| buttonGroup | object | - | options 类型:透传给 @kne/button-group 的属性 |
rowSelection
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| type | 'checkbox' | 'radio' |
- | 选择类型 |
| selectedRowKeys | array | - | 已选中的行 key 列表 |
| onChange | function | - | 选中变化回调 (selectedRowKeys, id, { context, checked }) => void |
| allowSelectedAll | boolean | - | 是否允许全选(仅 checkbox 模式) |
| isSelectedAll | boolean | - | 是否全选状态 |
| onIsSelectAllChange | function | - | 全选状态变化回调 |
| checkRelation | 'parent' | 'all' | 'independent' |
'parent' |
树形 checkbox 父子勾选关联。仅 dataType 为 tree / treeList 时生效:parent 勾父级时子级 UI 全勾但值只留父级,取消子级会拆分父级,半选;all 值含父级与全部子孙,半选;independent 父子互不影响 |
树形 checkRelation 补充说明:
parent(默认):勾选节点会写入该 key 并去掉其子孙 key;子级全部勾选后折叠为父级;父级半选表示部分子孙被选all:勾选/取消同步增删子孙 key;子级全选时父级也写入值;父级半选independent:点哪个改哪个,无半选联动- 全选:
parent写入各根节点 key;all/independent写入全部节点 key(含收起子行)
静态属性
| 属性 | 类型 | 说明 |
|---|---|---|
| TableView.Header | Component | 表头组件,可单独使用 |
| TableView.useSelectedRow | Hook | 行选择 Hook |
| TableView.useSort | Hook | 排序 Hook |
| TableView.sortDataSource | function | 本地排序工具函数 |
示例
<TableView
dataSource={dataSource}
columns={[
{ name: 'id', title: '订单编号', width: 160, renderType: 'small' },
{ name: 'customerName', title: '客户名称', renderType: 'main' },
{ name: 'amount', title: '金额', renderType: 'amount', format: 'number-style:decimal-suffix:元' }
]}
rowSelection={{
type: 'checkbox',
selectedRowKeys,
onChange: setSelectedRowKeys
}}
/>
完整示例见文档 TableView。
useSelectedRow
行选择 Hook,用于配合 TableView 的 rowSelection。
参数
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| rowKey | string | function | 'id' |
行唯一标识 |
| type | 'checkbox' | 'radio' |
'checkbox' |
选择类型 |
返回值
| 属性 | 类型 | 说明 |
|---|---|---|
| selectedRowKeys | array | 已选行 key 列表 |
| selectedRows | array | 已选行数据 |
| onSelect | function | (item, checked) => void |
| onSelectAll | function | (checked, selected, items) => void |
| setSelectedRows | function | 直接设置已选行数据 |
| setSelectedRowKeys | function | (keys, dataSource) => void |
| clearSelectedRows | function | 清空选择 |
| getRowSelection | function | (dataSource, extra?) => rowSelection 生成 rowSelection 配置 |
示例
const { selectedRowKeys, selectedRows, getRowSelection, clearSelectedRows } = TableView.useSelectedRow({ rowKey: 'id' });
<TableView dataSource={dataSource} columns={columns} rowSelection={getRowSelection(dataSource)} />;
完整示例见文档 useSelectedRow。
useSort
排序 Hook,配合 TableView 的 sortRender 实现表头排序。
参数
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| sort | array | - | 受控排序值 [{ name, sort: 'ASC' | 'DESC' }] |
| defaultSort | array | [] |
默认排序 |
| onSortChange | function | - | 排序变化回调 (sort) => void |
返回值
| 属性 | 类型 | 说明 |
|---|---|---|
| sort | array | 当前排序配置 |
| setSort | function | 设置排序 |
| sortRender | function | ({ name, single }) => ReactNode,传给 TableView 表头 |
| mobileSortToolbar | function | ({ columns }) => ReactNode,传给 TableView 移动端工具栏右侧 |
columns.sort
| 值 | 说明 |
|---|---|
true |
开启排序,默认单列模式 |
{ single: true } |
单列排序,切换列时清除其他列 |
{ single: false } |
多列排序 |
sortDataSource
本地排序工具函数:sortDataSource(dataSource, sort, columns)。支持数字排序与中文 localeCompare 排序,空值始终排在末尾。
示例
const { sort, sortRender, mobileSortToolbar } = TableView.useSort({ onSortChange: console.log });
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort, dataSource]);
<TableView dataSource={sortedData} columns={columns} sortRender={sortRender} mobileSortToolbar={mobileSortToolbar} />;
完整示例见文档 useSort。
renderType
通过 columns.renderType 声明列的渲染方式,无需手写 render。可与尺寸修饰词组合。若同时配置了 columns.render,则以 render 为准(优先级最高),renderType 仍可注入 width / min / max 等维度。
内置类型
| 类型 | 说明 | 默认宽度 |
|---|---|---|
main |
主信息列,支持 primary / hover / onClick,自动省略 |
300px |
amount |
金额列,右对齐,自动省略 | 140px |
options |
操作列,铺满单元格,配合 @kne/button-group |
180px |
tag |
单个 Tag,getValueOf 返回 { type, text } |
140px |
status |
状态 Badge,getValueOf 返回 { type, text } |
100px |
tagList |
多个 Tag 列表 | 300px |
list |
文本列表,可用 split 指定分隔符 |
200px |
description |
长文本描述列,自动省略 | 400px |
尺寸修饰词
可与类型组合,如 tag-short、status-small、main-large:
| 修饰词 | 说明 | 宽度 |
|---|---|---|
short |
缩短列宽 | 120px |
small |
最小列宽 | 100px |
large |
放大列宽 | 300px |
options 列按钮配置
getValueOf 返回按钮配置数组,每项支持:
| 属性 | 类型 | 说明 |
|---|---|---|
| children | ReactNode | 按钮文案 |
| onClick | function | 点击回调 |
| isDelete | boolean | 是否为删除操作(弹出确认) |
| message | string | 删除确认文案 |
| hidden | boolean | 是否隐藏 |
完整示例见文档 renderType。
format
通过 columns.format 声明式格式化展示值,支持链式组合(空格分隔多个格式化器)。
内置格式化器
| 名称 | 参数 | 说明 | 示例 |
|---|---|---|---|
date |
模板 | 日期格式化 | format: 'date' 或 format: 'date-YYYY/MM/DD' |
datetime |
模板 | 日期时间格式化 | format: 'datetime' |
dateRange |
模板, allow | 日期范围 | format: 'dateRange-YYYY-MM-DD-allow' |
boolean |
trueValue | 布尔值转是/否 | format: 'boolean' |
number |
见下方 | 数字格式化 | format: 'number-style:decimal-suffix:元' |
money |
单位 | 金额拼接 | format: 'money-元' |
number 格式化参数
通过 calcArgs 解析,格式为 number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| style | string | 'decimal' |
Intl 数字样式 |
| unit | number | 1 |
除数 |
| maximumFractionDigits | number | 2 |
最大小数位 |
| useGrouping | boolean | true |
是否使用千分位 |
| roundingMode | string | 'halfExpand' |
舍入模式 |
| suffix | string | '' |
后缀 |
也可传入函数:format: (value, { dataSource, column, context }) => string。
preset / globalParams
全局参数预设,用于定制 renderType 映射、列宽与标签颜色。
import { preset } from '@kne/table-view';
preset({
renderTypeSize: {
main: { width: 400, min: 200, max: 600 }
},
renderMobile: {
orderCard: ({ renderBody }) => <div>{renderBody()}</div>
},
tagTypeColors: {
custom: '#1890ff'
},
statusTypeColors: {
custom: '#52c41a'
}
});
| 属性 | 类型 | 说明 |
|---|---|---|
| renderType | object | 自定义 renderType 映射 |
| renderTypeSize | object | 覆盖内置类型的 width / min / max |
| renderMobile | object | 注册移动端渲染函数,renderMobile="name" 时按名称查找 |
| tagTypeColors | object | Tag 颜色映射 |
| statusTypeColors | object | Status 颜色映射 |
列解析工具
| 函数 | 说明 |
|---|---|
resolveColumns(columns) |
批量解析列配置,自动注入 render、width、ellipsis 等 |
resolveColumn(column) |
解析单个列配置 |
parseRenderType(renderType) |
解析 renderType 字符串,返回 { type, size } |
resolveRenderType(renderType) |
解析 renderType 并返回完整维度信息 |
getColumnRender(column) |
获取列的 render 函数 |
getRenderTypeNames() |
获取所有已注册的 renderType 名称 |
isOptionsColumn(column) |
判断是否为操作列 |
resolveRenderMobile(renderMobile) |
解析 renderMobile 配置,string 时从 preset 查找 |
isRenderMobileActive(renderMobile, isMobile) |
判断当前是否应启用移动端专用渲染 |
列值计算
| 函数 | 说明 |
|---|---|
computeColumnsValue(options) |
计算列值,处理 format、display、空值等 |
computeDisplay(options) |
根据列值渲染展示内容 |
computeColumnsDisplay(options) |
批量计算并渲染列展示内容 |
computeColumnsValue 参数:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| columns | array | - | 列配置 |
| dataSource | object | - | 行数据 |
| context | object | - | 渲染上下文 |
| emptyIsPlaceholder | boolean | - | 空值是否显示占位符 |
| valueIsEmpty | function | - | 空值判断函数 |
| placeholder | string | - | 占位符 |
| removeEmpty | boolean | true |
是否过滤 display 为 false 的列 |
渲染工具
| 函数 | 说明 |
|---|---|
renderCellContent(content, column, className) |
渲染单元格内容 |
getColumnEllipsis(column) |
获取列的 ellipsis 配置 |
wrapColumnHeaderTitle(title) |
包装表头标题,支持省略 |
renderColumnTitle(title, column, sortRender) |
渲染带排序的表头标题 |
getTagColor(type) |
获取 Tag 颜色 |
renderTagItem(value) |
渲染单个 Tag |
renderTagList(value) |
渲染 Tag 列表 |
getStatusType(type) |
获取 Status 类型 |
renderStatusItem(value) |
渲染 Status |
列宽工具
| 函数 | 说明 |
|---|---|
parseColumnWidth(width) |
解析列宽值为数字(px) |
getColumnLayout(column, options) |
获取列布局信息(widthBased、fillRemaining、style) |
getGridTemplateColumns(columns, options) |
生成 CSS Grid 模板列 |
getTreeSelectionColumnWidth(options) |
计算树形/选择列宽度 |
hasColumnSpan(column) |
是否设置了 span |
hasColumnWidth(column, columns) |
是否设置了 width |
树形数据工具
| 函数 | 说明 |
|---|---|
isTreeDataType(dataType) |
是否为 tree / treeList |
buildTreeFromList(list, options) |
扁平列表按 parentKey 组装为树 |
normalizeTreeData(dataSource, options) |
按 dataType 归一化为树 |
flattenVisibleTree(tree, options) |
按展开状态展平可见行 |
flattenAllTree(tree, options) |
展平全部节点(含收起子行,用于全选) |
collectExpandableKeys(tree, options) |
收集所有可展开节点 key |
toggleExpandedKey(expandedKeys, key, expandableKeys) |
切换单个节点展开状态 |
nodeCanExpand(item, options) |
是否显示展开三角(含 hasChildren) |
mergeTreeChildren(dataSource, children, options) |
懒加载后合并子节点(tree/treeList) |
getTreeBreadcrumbItems(key, maps) |
移动端面包屑:根到当前节点的节点列表 |
buildTreeKeyMaps(tree, options) |
构建父子/子孙 key 映射 |
toggleTreeCheck(options) |
按 checkRelation 切换勾选 keys |
getTreeCheckState(key, selectedKeys, mode, maps) |
返回 { checked, indeterminate } |
normalizeParentKeys(keys, maps) |
parent 模式折叠全选子树为父 key |
buildSelectAllKeys / buildClearSelectAllKeys |
树形全选 / 取消全选 keys |
常量
| 常量 | 说明 |
|---|---|
RENDER_TYPE_NAMES |
所有 renderType 名称数组 |
SIZE_NAMES |
尺寸修饰词数组 ['short', 'small', 'large'] |