@kne/table-view
v0.1.19
Published
A React table view component with column rendering, computed columns, formatting and sorting utilities
Readme
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 说明
