@kne/table-page
v0.1.11
Published
A React table page component based on Ant Design, supporting column config, filter, sort and batch operations.
Downloads
1,811
Readme
table-page
描述
A React table page component based on Ant Design, supporting column config, filter, sort and batch operations.
安装
npm i --save @kne/table-page概述
@kne/table-page 是一个基于 React 和 Ant Design 的表格页面组件库,提供开箱即用的数据表格解决方案。组件库围绕表格的常见业务场景,封装了数据加载、分页、排序、行选择、列配置、筛选搜索、批量操作等能力,帮助开发者快速构建功能完善的表格管理页面。
核心组件
TablePage
表格页面主组件,基于 @kne/react-fetch 封装数据请求与分页逻辑。内置两种渲染模式:
Table模式(默认):基于 antdTable,支持列宽拖动、字段显示/隐藏、分组表头、粘性表头等TableView模式:基于@kne/table-viewCSS Grid,适合移动端或卡片式表格场景
通过 loader 或 url 配置数据源,通过 dataFormat 适配不同的接口数据结构。分页器渲染在表格外侧,翻页默认采用 reload 方式(不显示全屏 loading)。
同时内置了顶部工具栏(TableToolbar),整合筛选、搜索、Tab 分类、批量操作等能力:
- 筛选(filter):基于
@kne/react-filter的FilterLines,支持多行多字段组合筛选,筛选值变化时自动reload并回到第 1 页 - 搜索(search):基于
@kne/react-filter的SearchInput,支持关键词搜索与防抖自动提交,与筛选器共享筛选值状态 - Tab(tab):顶部分类切换,默认「全部」,选中值写入 filter value 并显示在已选标签;桌面端在表格边框外侧,移动端显示在 SearchInput 下方;可通过
tabProps透传 antd Tabs 属性 - 批量操作(batchActions):配合
rowSelection和useSelectedRow,提供下拉菜单形式的批量操作(如批量导出、批量通知),未选中时自动禁用 - 已选筛选值展示:工具栏下方展示当前生效的筛选条件标签,支持快速清除
Table
基于 antd Table 的表格组件,与 TableView 共享相同的 columns、rowSelection 等 API。额外提供以下增强能力:
- 列宽拖动:悬停表头列右侧拖动手柄可调整列宽,支持
min/max限制 - 列配置面板:通过表头最后一列的设置图标,可显示/隐藏字段、拖拽排序
- 配置持久化:设置
name后,列宽和显示状态自动保存到 localStorage - 分组表头:通过
groupHeader配置实现多级表头结构 - 浮动横向滚动条:当表格宽度超出容器时,底部自动显示横向滚动条(通过
horizontalScroller控制)
TableView
基于 @kne/table-view 的 CSS Grid 表格视图组件。相比于 Table,它更轻量灵活,适合需要自定义渲染、移动端卡片展示的场景。支持:
- 基于 24 栅格的列宽分配(
span属性) - CSS Grid 自动布局,内容超出时自动撑开
- 行选择(checkbox 多选 / radio 单选)
- 行点击事件
- 通过
render属性自定义渲染,可拆分表头和表体
核心 Hooks
useSelectedRow
行选择 Hook,支持多选(checkbox)和单选(radio)两种模式。提供:
getRowSelection(dataSource)生成标准rowSelection配置,可直接传入Table或TableViewselectedRowKeys和selectedRows追踪选中状态setSelectedRowKeys(keys, dataSource)从 key 列表反查完整行数据clearSelectedRows()一键清空选择
适用于批量操作(批量删除、批量导出等)和单选场景(详情查看、关联选择等)。
useSort
排序 Hook,配合 Table/TableView 的 sortRender 实现表头排序交互。支持:
- 单列排序(
sort: true或sort: { single: true }):切换列时自动清除其他列的排序 - 多列排序(
sort: { single: false }):允许多列同时排序 - 排序状态循环切换:DESC → ASC → 取消
sortDataSource(dataSource, sort, columns)工具函数,支持本地排序(包含中文排序)
渲染逻辑
双模式:Table / TableView
TablePage 通过 type 切换底层表格实现:
| 模式 | 底层 | 适用场景 |
|------|------|----------|
| Table(默认) | antd Table | 桌面端完整表格能力:列宽拖动、列配置、分组表头、粘性表头、总结栏 |
| TableView | @kne/table-view CSS Grid | 轻量栅格表格、移动端、卡片式展示 |
两种模式共享 columns、rowSelection、sortRender、renderType 等 API,列渲染管线统一来自 @kne/table-view。
列单元格渲染管线
无论 Table 还是 TableView,单元格内容均走同一套流程(Table 在 antd columns[].render 内调用):
resolveColumns:解析renderType,注入内置render与width/min/max/ellipsiscomputeColumnsValue:getValueOf取值 →format格式化 → 按display/ 空值规则过滤computeDisplay:空值占位;非空调用列renderrenderCellContent:按ellipsis/cellFullWidth输出最终节点
列渲染优先级:column.render(最高)> renderType 内置渲染 > 原始格式化值。render 与 renderType 共存时,后者仅提供列宽等布局维度。
桌面端:antd Table
Table 将解析后的列映射为 antd columns,在 render 回调中复用上述管线。额外能力:
useTableConfig管理列宽拖动、显示/隐藏、localStorage 持久化useGroupHeader生成分组表头rowSelection映射为 antd 行选择(含allowSelectedAll全选)render={({ header, renderBody }) => ...}可自定义表格外层,renderBody()返回完整 antd Table
移动端:renderMobile
Table 与 TableView 均支持 renderMobile,移动端判断使用 useIsMobile()(768px)。激活后 Table 不再渲染 antd Table,委托 TableView 处理:
| renderMobile 值 | 行为 |
|-------------------|------|
| true | 默认卡片 List:每行一张卡片,字段列「标题 + 内容」纵向排列,options 操作列靠右(紧凑「⋯」入口) |
| function | 完全接管移动端渲染,签名 ({ header, renderBody, ...props }) => ReactNode;可调用 renderBody() 复用默认卡片 |
| string | 从 preset({ renderMobile: { [name]: fn } }) 查找;未注册则视为未开启,回退普通表格 |
桌面端不受 renderMobile 影响:Table 仍走 antd Table,TableView 仍走 CSS Grid 或 render。
列渲染类型系统
通过 renderType 属性,可以用声明式的方式定义列的渲染样式,无需手写 render 函数。内置以下 render 类型:
| 类型 | 说明 |
|------|------|
| main | 主要内容列,自动省略号,较大宽度 |
| options | 操作列,铺满单元格 |
| enum | 枚举值渲染,自动映射 color/text |
| tag | 标签渲染,单个 Tag 组件 |
| status | 状态渲染,antd Badge 组件 |
| tagList | 标签列表渲染,多个 Tag 组件 |
| amount | 金额列,右对齐,自动省略号 |
| list | 列表渲染,自动省略号 |
| description | 描述文本,大宽度,自动省略号 |
支持尺寸修饰符:
short:缩小宽度(约 120px)small:最小宽度(约 100px)large:放大宽度(约 300px)
例如 renderType: "enum-small" 表示枚举值 + 小尺寸列。维度(width、min、max、ellipsis)可通过 globalParams.renderTypeSize 全局定制。
默认导出 getTagColor、renderTagItem、renderTagList、getStatusType、renderStatusItem 工具函数,用于 Tag / Status 相关渲染。
其他导出
| 导出项 | 说明 |
|--------|------|
| tableLocalApis | 基于 localStorage 的列配置存取 API,可替换为服务端存储 |
| useTableConfig | 列配置 Hook,提供列宽、显示状态的管理能力 |
| preset / globalParams | 全局参数预设,用于设置 renderType 映射和标签颜色等全局配置 |
| Ellipsis | 超出省略组件,基于 antd Typography |
| label | 标签组件 |
| sortDataSource | 客户端排序工具函数 |
使用场景
- 后台管理系统:订单管理、用户列表、商品管理等 CRUD 页面
- 数据报表:配合排序、分页、总结栏展示统计数据
- 列表配置页:需要用户自定义列宽、显示字段的表格场景
- 移动端适配:
renderMobile启用卡片 List,或TableView模式做栅格式展示
示例
示例样式
@use '~@kne/responsive-utils/scss' as resp;
// 手机预览下给示例内容左右留白,便于观察表格/卡片边框
@include resp.mobile-container {
.example-driver-runner {
padding-inline: 16px;
box-sizing: border-box;
}
}示例代码
- TablePage
- 表格页面组件,基于 @kne/react-fetch 实现数据加载与分页,支持 sticky 固定表头、useSort 服务端排序、renderMobile 移动端卡片、tab 分类切换、列配置、总结栏;空数据(total 为 0)时不显示分页器
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd),_ReactFilter(@kne/react-filter)[import * as _ReactFilter from "@kne/react-filter"],(@kne/react-filter/dist/index.css)
const { default: TablePage, Table } = _TablePage;
const { fields } = _ReactFilter;
const { SuperSelectFilterItem } = fields;
const { Table: AntTable, Flex, Tag, Button, Space, Switch, message } = antd;
const { useMemo, useState } = React;
const TOTAL = 156;
const range = (start, end) => Array.from({ length: end - start }, (_, i) => start + i);
const surnames = ['张', '李', '王', '刘', '陈'];
const givenNames = ['伟', '强', '敏', '磊', '杰', '婷', '娜', '静', '丽', '娟'];
const departments = ['技术研发部', '产品设计部', '市场营销部', '人力资源部', '财务部'];
const positions = ['工程师', '高级工程师', '经理', '总监', '专员'];
const educations = ['本科', '硕士', '博士', '大专'];
const performances = ['A', 'B', 'C', 'S'];
const statusMap = {
active: { type: 'success', text: '在职' },
vacation: { type: 'warning', text: '休假' },
resigned: { type: 'default', text: '离职' },
probation: { type: 'processing', text: '试用期' }
};
const perfMap = {
S: { type: 'success', text: 'S' },
A: { type: 'processing', text: 'A' },
B: { type: 'warning', text: 'B' },
C: { type: 'error', text: 'C' }
};
const departmentOptions = departments.map(item => ({ value: item, label: item }));
const statusOptions = Object.entries(statusMap).map(([value, { text }]) => ({ value, label: text }));
const positionOptions = positions.map(item => ({ value: item, label: item }));
const buildEmployee = index => {
const statusKeys = ['active', 'vacation', 'resigned', 'probation'];
return {
id: `EMP${String(index + 1).padStart(4, '0')}`,
employeeNo: `EMP-2024-${String(index + 1).padStart(4, '0')}`,
name: `${surnames[index % surnames.length]}${givenNames[index % givenNames.length]}`,
department: departments[index % departments.length],
position: positions[index % positions.length],
status: statusKeys[index % statusKeys.length],
email: `employee${index + 1}@company.com`,
phone: `138${String(index).padStart(8, '0')}`,
joinDate: `2023-${String((index % 12) + 1).padStart(2, '0')}-${String((index % 28) + 1).padStart(2, '0')}`,
workYears: Math.floor(index / 12) + 1,
salary: `${15 + (index % 20)}K-${20 + (index % 20)}K`,
education: educations[index % educations.length],
performance: performances[index % performances.length]
};
};
const columns = [
{
name: 'employeeNo',
title: '工号',
width: 180,
min: 120,
max: 240,
fixed: 'left',
sort: { single: true },
renderType: 'main',
primary: true,
onClick: ({ item, colItem }) => {
message.info(`查看员工:${colItem.name}(${item})`);
}
},
{
name: 'name',
title: '姓名',
width: 100,
min: 80,
max: 160,
sort: true,
renderType: 'main',
onClick: ({ item, colItem }) =>
new Promise(resolve => {
const hide = message.loading(`正在加载 ${item} 的详情…`, 0);
setTimeout(() => {
hide();
message.success(`${colItem.department} · ${colItem.position}`);
resolve();
}, 600);
})
},
{ name: 'department', title: '部门', width: 150, min: 120, max: 240, sort: true },
{ name: 'position', title: '职位', width: 120, min: 100, max: 200 },
{
name: 'status',
title: '状态',
renderType: 'status',
getValueOf: item => statusMap[item.status] || { type: 'default', text: item.status }
},
{ name: 'performance', title: '绩效', width: 80, min: 70, max: 120, renderType: 'tag', getValueOf: item => perfMap[item.performance] || { type: 'default', text: item.performance } },
{ name: 'phone', title: '手机号', width: 140, min: 120, max: 180, render: value => value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3') },
{ name: 'email', title: '邮箱', width: 200, min: 160, max: 320, ellipsis: true },
{ name: 'joinDate', title: '入职日期', width: 120, min: 100, max: 160, format: 'date', sort: true },
{ name: 'workYears', title: '工龄', width: 90, min: 70, max: 120, sort: true, render: value => `${value}年` },
{ name: 'salary', title: '薪资范围', width: 120, min: 100, max: 180, hidden: true },
{ name: 'education', title: '学历', width: 90, min: 70, max: 120, hidden: true },
{
name: 'options',
title: '操作',
renderType: 'options',
fixed: 'right',
width: 160,
min: 120,
max: 200,
getValueOf: item => {
const actions = [
{ children: '查看', onClick: () => message.info(`查看 ${item.name}`) },
{ children: '编辑', onClick: () => message.info(`编辑 ${item.name}`) }
];
if (item.status !== 'resigned') {
actions.push({
children: '离职办理',
onClick: () => message.warning(`办理离职 ${item.name}`)
});
}
return actions;
}
}
];
const sortFieldLabels = {
employeeNo: '工号',
name: '姓名',
department: '部门',
joinDate: '入职日期',
workYears: '工龄'
};
const normalizeFilterValue = value => {
if (value == null) {
return value;
}
return Array.isArray(value) ? value[0] : value;
};
const applyFilters = (employees, data, requestParams) => {
const params = Object.assign({}, requestParams?.data, data);
let result = employees;
if (params.keyword) {
const keyword = String(params.keyword).toLowerCase();
result = result.filter(item => item.employeeNo.toLowerCase().includes(keyword) || item.name.includes(params.keyword));
}
const department = normalizeFilterValue(params.department);
if (department) {
result = result.filter(item => item.department === department);
}
const status = normalizeFilterValue(params.status);
if (status) {
result = result.filter(item => item.status === status);
}
const position = normalizeFilterValue(params.position);
if (position) {
result = result.filter(item => item.position === position);
}
return result;
};
const SortState = ({ sort }) => (
<div style={{ padding: '12px', background: '#f5f5f5', borderRadius: 8, fontSize: 13 }}>
当前排序:
{sort.length ? (
sort.map(item => (
<Tag key={item.name} color="blue" style={{ marginLeft: 8 }}>
{sortFieldLabels[item.name] || item.name} {item.sort}
</Tag>
))
) : (
<span style={{ marginLeft: 8, color: '#999' }}>无</span>
)}
</div>
);
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>loader</code> 模拟分页接口,请求参数为 <code>data.currentPage</code>、<code>data.perPage</code>。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="green">分页</Tag>
分页器渲染在表格外侧,翻页时以 <code>reload</code> 方式请求;<code>pageSize</code> 会持久化到 localStorage;当 <code>total</code> 为 0(无数据)时不显示分页器。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="gold">筛选</Tag>
顶部工具栏集成 <code>filter</code>、<code>search</code>、<code>batchActions</code>;筛选变化自动 <code>reload</code> 并回到第 1 页。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="lime">Tab</Tag>
通过 <code>tab</code> 配置顶部分类切换(默认「全部」),选中值写入 filter value 并显示在已选标签;桌面端在表格边框外,移动端在 SearchInput 下方;可用 <code>tabProps</code> 透传 Tabs 属性(如 <code>tabBarExtraContent</code>)。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="orange">列配置</Tag>
设置 <code>name</code> 开启列宽拖动与显示/隐藏,「薪资范围」「学历」默认隐藏;状态列使用 <code>renderType="status"</code>,绩效列使用 <code>renderType="tag"</code>,操作列使用 <code>renderType="options"</code> 且 <code>fixed="right"</code>。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="cyan">排序</Tag>
配合 <code>Table.useSort</code> 与 <code>sortRender</code>、<code>mobileSortToolbar</code>,在 <code>onSortChange</code> 中调用 <code>reload</code> 传排序参数,与翻页一样不闪烁。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="volcano">移动端</Tag>
设置 <code>renderMobile</code> 后,手机预览下启用卡片 List(含全选、排序工具栏);桌面端仍为 antd Table。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="geekblue">固定表头</Tag>
设置 <code>sticky</code> 与 <code>scroll.y</code>,表体在固定高度内滚动时表头保持可见;横向滚动配合 <code>scroll.x</code>。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="magenta">单元格点击</Tag>
列配置 <code>onClick</code>(配合 <code>renderType="main"</code>、<code>primary</code> / <code>hover</code>),仅可点击单元格 hover 时显示手型;工号列同步演示异步点击 loading。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="purple">总结栏</Tag>
<code>summary</code> 回调可拿到 <code>data</code>、<code>requestParams</code> 等 fetch 上下文。
</div>
</div>
);
const BaseExample = () => {
const tableRef = React.useRef();
const [empty, setEmpty] = useState(false);
const emptyRef = React.useRef(false);
const allEmployees = useMemo(() => range(0, TOTAL).map(buildEmployee), []);
const { selectedRows, getRowSelection } = Table.useSelectedRow({ rowKey: 'id' });
const { sort, sortRender, mobileSortToolbar } = Table.useSort({
defaultSort: [{ name: 'joinDate', sort: 'DESC' }],
onSortChange: newSort => {
tableRef.current?.reload({
data: { currentPage: 1, sort: newSort }
});
}
});
return (
<Flex vertical gap={16}>
<Tips />
<SortState sort={sort} />
<Space wrap>
<Flex align="center" gap={8}>
<Switch
checked={empty}
onChange={checked => {
emptyRef.current = checked;
setEmpty(checked);
tableRef.current?.reload({ data: { currentPage: 1 } });
}}
/>
<span>{empty ? '空数据(无分页)' : '有数据(显示分页)'}</span>
</Flex>
<Button
onClick={() => {
tableRef.current?.reload({
data: { currentPage: 1 }
});
}}
>
重新加载(回到第 1 页)
</Button>
<Button
onClick={() => {
tableRef.current?.refresh();
}}
>
刷新当前页
</Button>
</Space>
<TablePage
ref={tableRef}
name="demo-employee-table"
sticky
scroll={{ x: 1600, y: 400 }}
size="large"
renderMobile
sortRender={sortRender}
mobileSortToolbar={mobileSortToolbar}
rowSelection={getRowSelection(allEmployees)}
selectedRows={selectedRows}
search={{ name: 'keyword', label: '关键词', placeholder: '搜索工号/姓名', style: { width: 220 } }}
tab={{
name: 'position',
label: '职位',
list: positionOptions
}}
tabProps={{
tabBarExtraContent: (
<Button type="link" size="small" onClick={() => message.info('新增职位')}>
新增职位
</Button>
)
}}
filter={{
list: [
[
{
type: SuperSelectFilterItem,
props: { name: 'department', label: '部门', single: true, options: departmentOptions }
},
{
type: SuperSelectFilterItem,
props: { name: 'status', label: '状态', single: true, options: statusOptions }
}
]
],
displayLine: 1
}}
batchActions={[
{
key: 'export',
label: '批量导出',
onClick: ({ selectedRowKeys }) => {
message.info(`正在导出 ${selectedRowKeys.length} 名员工`);
}
},
{
key: 'notify',
label: '批量通知',
onClick: ({ selectedRowKeys }) => {
message.success(`已通知 ${selectedRowKeys.length} 名员工`);
}
}
]}
pagination={{
open: true,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100']
}}
dataFormat={data => ({
list: data.pageData,
total: data.totalCount,
data
})}
loader={({ data, requestParams }) => {
if (emptyRef.current) {
return new Promise(resolve => {
setTimeout(() => resolve({ pageData: [], totalCount: 0 }), 400);
});
}
const currentPage = Number(data?.currentPage ?? requestParams?.data?.currentPage) || 1;
const perPage = Number(data?.perPage ?? requestParams?.data?.perPage) || 20;
const sortParams = data?.sort ?? requestParams?.data?.sort ?? [{ name: 'joinDate', sort: 'DESC' }];
const filteredEmployees = applyFilters(allEmployees, data, requestParams);
const sortedEmployees = sortParams.length ? Table.sortDataSource(filteredEmployees, sortParams, columns) : filteredEmployees;
const startIndex = (currentPage - 1) * perPage;
return new Promise(resolve => {
setTimeout(() => {
resolve({
pageData: sortedEmployees.slice(startIndex, startIndex + perPage),
totalCount: filteredEmployees.length
});
}, 400);
});
}}
columns={columns}
summary={({ pageData, data }) => {
const totalCount = data?.totalCount || 0;
return (
<AntTable.Summary fixed>
<AntTable.Summary.Row>
<AntTable.Summary.Cell index={0} colSpan={5}>
<strong>当前页统计</strong>
</AntTable.Summary.Cell>
<AntTable.Summary.Cell index={5}>
<strong>{pageData.length} 人</strong>
</AntTable.Summary.Cell>
<AntTable.Summary.Cell index={6} colSpan={7}>
<strong>总员工数: {totalCount} 人</strong>
</AntTable.Summary.Cell>
</AntTable.Summary.Row>
</AntTable.Summary>
);
}}
/>
</Flex>
);
};
render(<BaseExample />);
- TablePage sticky scroll
- 自包含演示区:sticky + getScrollContainer 由区内页面滚动触发表头吸顶(非 scroll.y);模拟导航在区内 sticky,不遮挡文档站顶栏
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd),_ReactFilter(@kne/react-filter)[import * as _ReactFilter from "@kne/react-filter"],(@kne/react-filter/dist/index.css)
const { default: TablePage } = _TablePage;
const { fields } = _ReactFilter;
const { SuperSelectFilterItem } = fields;
const { Flex, Tag } = antd;
const { useRef, useMemo } = React;
const NAV_HEIGHT = 56;
const DEMO_HEIGHT = 600;
const TOTAL = 80;
const statusMap = {
active: { type: 'success', text: '在职' },
vacation: { type: 'warning', text: '休假' },
resigned: { type: 'default', text: '离职' },
probation: { type: 'processing', text: '试用期' }
};
const departments = ['技术研发部', '产品设计部', '市场营销部', '人力资源部', '财务部'];
const departmentOptions = departments.map(item => ({ value: item, label: item }));
const statusOptions = Object.entries(statusMap).map(([value, { text }]) => ({ value, label: text }));
const normalizeFilterValue = value => {
if (value == null) {
return value;
}
return Array.isArray(value) ? value[0] : value;
};
const applyFilters = (employees, data, requestParams) => {
const params = Object.assign({}, requestParams?.data, data);
let result = employees;
if (params.keyword) {
const keyword = String(params.keyword).toLowerCase();
result = result.filter(item => item.employeeNo.toLowerCase().includes(keyword) || item.name.includes(params.keyword));
}
const department = normalizeFilterValue(params.department);
if (department) {
result = result.filter(item => item.department === department);
}
const status = normalizeFilterValue(params.status);
if (status) {
result = result.filter(item => item.status === status);
}
return result;
};
const buildEmployee = index => {
const statusKeys = ['active', 'vacation', 'resigned', 'probation'];
return {
id: `EMP${String(index + 1).padStart(4, '0')}`,
employeeNo: `EMP-2024-${String(index + 1).padStart(4, '0')}`,
name: `员工${index + 1}`,
department: departments[index % departments.length],
position: ['工程师', '经理', '专员'][index % 3],
status: statusKeys[index % statusKeys.length],
joinDate: `2024-${String((index % 12) + 1).padStart(2, '0')}-15`
};
};
const allEmployees = Array.from({ length: TOTAL }, (_, index) => buildEmployee(index));
const columns = [
{ name: 'employeeNo', title: '工号', width: 160, min: 120, max: 220, fixed: 'left', renderType: 'small' },
{ name: 'name', title: '姓名', width: 100, renderType: 'main' },
{ name: 'department', title: '部门', width: 150 },
{ name: 'position', title: '职位', width: 120 },
{
name: 'status',
title: '状态',
width: 100,
renderType: 'status',
getValueOf: item => statusMap[item.status] || { type: 'default', text: item.status }
},
{ name: 'joinDate', title: '入职日期', width: 120, format: 'date' }
];
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>
在下方<strong>灰色边框演示区</strong>内滚动(非 <code>scroll.y</code>);表头通过 <code>sticky</code> + <code>getScrollContainer</code> 吸顶。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="green">getScrollContainer</Tag>
指向演示区滚动容器;<code>scrollTopInset</code> 传入顶部导航占位高度(<code>{NAV_HEIGHT}px</code>),用于吸顶表头偏移与翻页滚回。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="gold">筛选栏</Tag>
顶部工具栏含 <code>search</code> 与 <code>filter</code>;筛选变化会 <code>reload</code> 并回到第 1 页,翻页后滚回工具栏顶部。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="purple">横向 Scroller</Tag>
表格底部未完全露出时,会在滚动容器底部显示横向滚动条(<code>horizontalScroller</code> 默认开启)。
</div>
<div>
<Tag style={TIP_TAG_STYLE} color="orange">操作提示</Tag>
在演示区内向下滚动,蓝色导航条会吸顶,表格表头应固定在其下方;翻页后滚回表格顶部。
</div>
</div>
);
const BaseExample = () => {
const scrollRef = useRef(null);
const loader = useMemo(
() =>
({ data, requestParams }) => {
const currentPage = Number(data?.currentPage) || 1;
const perPage = Number(data?.perPage) || 50;
const filteredEmployees = applyFilters(allEmployees, data, requestParams);
const start = (currentPage - 1) * perPage;
return new Promise(resolve => {
setTimeout(() => {
resolve({
pageData: filteredEmployees.slice(start, start + perPage),
totalCount: filteredEmployees.length
});
}, 200);
});
},
[]
);
return (
<Flex vertical gap={16}>
<Tips />
<div
style={{
border: '1px solid #f0f0f0',
borderRadius: 8,
overflow: 'hidden',
background: '#fff'
}}
>
<div
ref={scrollRef}
style={{
height: DEMO_HEIGHT,
overflow: 'auto',
boxSizing: 'border-box'
}}
>
<div
style={{
position: 'sticky',
top: 0,
zIndex: 100,
height: NAV_HEIGHT,
display: 'flex',
alignItems: 'center',
padding: '0 24px',
color: '#fff',
fontWeight: 500,
background: '#1677ff',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.12)'
}}
>
模拟顶部导航({NAV_HEIGHT}px)
</div>
<Flex vertical gap={16} style={{ padding: 16 }}>
<div
style={{
padding: '20px 24px',
background: '#f5f5f5',
borderRadius: 8,
color: '#666',
fontSize: 13
}}
>
在演示区内继续向下滚动 ↓
</div>
<div
style={{
height: 520,
borderRadius: 8,
background: 'linear-gradient(180deg, #f0f5ff 0%, #fff 100%)',
border: '1px dashed #d9d9d9',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#999'
}}
>
占位区域(模拟页面上方内容)
</div>
<TablePage
name="demo-table-page-sticky-scroll"
sticky
scrollTopInset={NAV_HEIGHT}
getScrollContainer={() => scrollRef.current}
scroll={{ x: 900 }}
search={{ name: 'keyword', label: '关键词', placeholder: '搜索工号/姓名', style: { width: 200 } }}
filter={{
list: [
[
{
type: SuperSelectFilterItem,
props: { name: 'department', label: '部门', single: true, options: departmentOptions }
},
{
type: SuperSelectFilterItem,
props: { name: 'status', label: '状态', single: true, options: statusOptions }
}
]
],
displayLine: 1
}}
pagination={{
open: true,
pageSize: 50,
cachePageSize: false,
showSizeChanger: true,
showQuickJumper: true
}}
dataFormat={data => ({
list: data.pageData,
total: data.totalCount
})}
loader={loader}
columns={columns}
/>
<div style={{ height: 80, color: '#999', fontSize: 13, textAlign: 'center' }}>演示区底部留白</div>
</Flex>
</div>
</div>
</Flex>
);
};
render(<BaseExample />);
- TableView
- 表格视图组件,支持行选择、列宽设置等
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd)
const { TableView } = _TablePage;
const { Flex, Tag } = 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: '138-0013-8000',
amount: 42500,
status: '已完成',
orderDate: '2024-01-15',
deliveryDate: '2024-01-17'
},
{
id: 'ORD20240115002',
customerName: '华为技术有限公司',
contact: '李四',
phone: '139-0014-9000',
amount: 85000,
status: '处理中',
orderDate: '2024-01-15',
deliveryDate: '2024-01-20'
},
{
id: 'ORD20240115003',
customerName: '阿里巴巴集团控股有限公司',
contact: '王五',
phone: '137-0015-7000',
amount: 120000,
status: '待发货',
orderDate: '2024-01-14',
deliveryDate: '2024-01-22'
},
{
id: 'ORD20240115004',
customerName: '北京字节跳动科技有限公司',
contact: '赵六',
phone: '136-0016-6000',
amount: 65000,
status: '已完成',
orderDate: '2024-01-13',
deliveryDate: '2024-01-16'
},
{
id: 'ORD20240115005',
customerName: '百度在线网络技术(北京)有限公司',
contact: '钱七',
phone: '135-0017-5000',
amount: 95000,
status: '已取消',
orderDate: '2024-01-12',
deliveryDate: ''
}
];
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>
<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 WithSelected = () => {
const [selectKeys, setSelectKeys] = useState([]);
const selectedOrder = dataSource.find(d => d.id === selectKeys[0]);
return (
<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={columns} rowSelection={{
type: 'radio', selectedRowKeys: selectKeys, onChange: setSelectKeys
}} />
</div>
);
};
const WithColumnWidth = () => {
const widthColumns = [
{ name: 'id', title: '订单编号', width: 180, renderType: 'small' },
{ name: 'customerName', title: '客户名称', width: '200px', renderType: 'main' },
{
name: 'amount',
title: '订单金额(元)',
width: 120,
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{
name: 'status',
title: '订单状态',
width: '100px',
renderType: 'status',
getValueOf: item => orderStatusMap[item.status] || { type: 'default', text: item.status }
}
];
return (
<div>
<div style={{ marginBottom: 12, color: '#666', fontSize: 13 }}>
通过 columns 的 <code>width</code> 设置列最小宽度,支持数字(如 <code>180</code>)或字符串(如 <code>'100px'</code>),内容超出时会自动撑开
</div>
<TableView dataSource={dataSource.slice(0, 3)} columns={widthColumns} />
</div>
);
};
const BaseExample = () => {
return (
<Flex vertical gap={16}>
<div style={{ background: '#f5f5f5', padding: '12px', borderRadius: '8px' }}>
订单列表 - 共 <strong>{dataSource.length}</strong> 个订单
</div>
<WithColumnWidth />
<TableView dataSource={dataSource} columns={columns} />
<WithCheckbox />
<WithSelected />
<div style={{ padding: '16px', background: '#fafafa', border: '1px dashed #d9d9d9', borderRadius: '8px' }}>
暂无订单数据
</div>
</Flex>
);
};
render(<BaseExample />);
- Table
- 基于 antd Table 的表格组件,支持列宽拖动、字段显示/隐藏,与 TableView 使用一致的 columns、rowSelection 等 API
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd)
const { Table } = _TablePage;
const { Flex, Tag } = 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: ''
}
];
const columns = [
{ name: 'id', title: '订单编号', width: 180, renderType: 'small' },
{ name: 'customerName', title: '客户名称', width: 200, renderType: 'main' },
{ 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: '订单金额(元)',
width: 120,
renderType: 'amount',
format: 'number-style:decimal-maximumFractionDigits:0-useGrouping:true-suffix:元'
},
{ name: 'orderDate', title: '下单日期', width: 110, format: 'date' },
{ name: 'deliveryDate', title: '预计送达', width: 110, 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>
<Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
<span>
已选 <strong>{selectKeys.length}</strong> 个订单,总金额 <strong style={{ color: '#52c41a' }}>¥{totalAmount.toLocaleString()}</strong>
</span>
</Flex>
<Table
dataSource={dataSource}
columns={columns}
rowSelection={{
type: 'checkbox',
allowSelectedAll: true,
selectedRowKeys: selectKeys,
onChange: setSelectKeys
}}
/>
</div>
);
};
const WithSelected = () => {
const [selectKeys, setSelectKeys] = useState([]);
const selectedOrder = dataSource.find(d => d.id === selectKeys[0]);
return (
<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>
<Table
dataSource={dataSource}
columns={columns}
rowSelection={{
type: 'radio',
selectedRowKeys: selectKeys,
onChange: setSelectKeys
}}
/>
</div>
);
};
const WithScroll = () => {
return (
<div>
<div style={{ marginBottom: 12, color: '#666', fontSize: 13 }}>
基于 antd Table 渲染,支持 <code>scroll</code>、<code>sticky</code> 等原生表格能力
</div>
<Table
dataSource={dataSource}
columns={columns}
sticky
scroll={{ x: 1200, y: 240 }}
/>
</div>
);
};
const BaseExample = () => {
return (
<Flex vertical gap={16}>
<div style={{ background: '#f5f5f5', padding: '12px', borderRadius: '8px' }}>
订单列表(antd Table)- 共 <strong>{dataSource.length}</strong> 个订单,与 TableView 使用相同的 columns / rowSelection API
</div>
<Table dataSource={dataSource} columns={columns} />
<WithCheckbox />
<WithSelected />
<Table dataSource={[]} columns={columns} />
<WithScroll />
</Flex>
);
};
render(<BaseExample />);
- useSelectedRow
- 行选择 Hook,配合 Table / TableView 实现多选、全选、批量操作与单选
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd)
const { Table, TableView } = _TablePage;
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 TableExample = () => {
const { selectedRowKeys, selectedRows, getRowSelection, clearSelectedRows } = Table.useSelectedRow({ rowKey: 'id' });
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>Table + useSelectedRow</div>
<BatchToolbar
selectedRowKeys={selectedRowKeys}
selectedRows={selectedRows}
clearSelectedRows={clearSelectedRows}
onBatchShip={() => {
message.success(`已批量发货 ${selectedRowKeys.length} 个订单`);
clearSelectedRows();
}}
onBatchExport={() => message.info(`正在导出 ${selectedRowKeys.length} 个订单`)}
/>
<Table dataSource={dataSource} columns={columns} rowSelection={getRowSelection(dataSource)} />
</div>
);
};
const TableViewExample = () => {
const { selectedRowKeys, selectedRows, getRowSelection, clearSelectedRows } = TableView.useSelectedRow({ rowKey: 'id' });
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>TableView + useSelectedRow</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 RadioExample = () => {
const { selectedRowKeys, selectedRows, getRowSelection } = Table.useSelectedRow({ rowKey: 'id', type: 'radio' });
const selectedOrder = selectedRows[0];
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>单选模式 type: 'radio'</div>
<div style={{ marginBottom: 12 }}>
当前选中:{selectedOrder ? `${selectedOrder.id}(${selectedOrder.customerName})` : '无'}
</div>
<Table dataSource={dataSource} columns={columns} rowSelection={getRowSelection(dataSource)} />
</div>
);
};
const BaseExample = () => {
return (
<Flex vertical gap={24}>
<TableExample />
<TableViewExample />
<RadioExample />
</Flex>
);
};
render(<BaseExample />);
- useSort
- 排序 Hook,配合 Table / TableView 实现表头排序、单列/多列排序与 sortDataSource 本地排序
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd)
const { Table, TableView } = _TablePage;
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 TableExample = () => {
const { sort, sortRender } = Table.useSort({
onSortChange: value => console.log('Table 排序变更:', value)
});
const sortedData = useMemo(() => Table.sortDataSource(dataSource, sort, columns), [sort]);
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>Table + useSort(金额、日期支持多列排序)</div>
<SortState sort={sort} />
<Table dataSource={sortedData} columns={columns} sortRender={sortRender} />
</div>
);
};
const TableViewExample = () => {
const { sort, sortRender } = TableView.useSort({
defaultSort: [{ name: 'orderDate', sort: 'DESC' }],
onSortChange: value => console.log('TableView 排序变更:', value)
});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>TableView + useSort(默认按下单日期降序)</div>
<SortState sort={sort} />
<TableView dataSource={sortedData} columns={columns} sortRender={sortRender} />
</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>
<TableExample />
<TableViewExample />
</Flex>
);
};
render(<BaseExample />);
- column ellipsis
- 表头 title 超出列宽自动省略、悬停 tooltip;单元格 ellipsis 配置基于 antd Typography 实现内容省略
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd)
const { Table, TableView } = _TablePage;
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 显示完整标题;带排序的列同样生效(<code>Table</code> / <code>TableView</code> 均支持,无需额外配置)。
</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 TableExample = () => {
const { sort, sortRender } = Table.useSort({});
const sortedData = useMemo(() => Table.sortDataSource(dataSource, sort, columns), [sort]);
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>Table(含排序表头省略)</div>
<Table dataSource={sortedData} columns={columns} sortRender={sortRender} scroll={{ x: 700 }} />
</div>
);
};
const TableViewExample = () => {
const { sort, sortRender } = TableView.useSort({});
const sortedData = useMemo(() => TableView.sortDataSource(dataSource, sort, columns), [sort]);
return (
<div>
<div style={{ marginBottom: 8, color: '#666' }}>TableView(含排序表头省略)</div>
<TableView dataSource={sortedData} columns={columns} sortRender={sortRender} />
</div>
);
};
const BaseExample = () => {
return (
<Flex vertical gap={24}>
<Tips />
<TableExample />
<TableViewExample />
</Flex>
);
};
render(<BaseExample />);
- renderType
- 列 renderType 配置:main / amount / tag / status / tagList / list / options / description / enum,支持与 short / small / large 尺寸修饰组合;配合 getValueOf、format、onClick 等列属性
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd)
const { Table, TableView } = _TablePage;
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' },
{
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',
fixed: 'right',
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>
<li><code>enum</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>
<div>
<div style={{ marginBottom: 8, color: '#666' }}>Table</div>
<Table dataSource={dataSource} columns={columns} scroll={{ x: 1800 }} />
</div>
<div>
<div style={{ marginBottom: 8, color: '#666' }}>TableView</div>
<TableView dataSource={dataSource} columns={columns} />
</div>
</Flex>
);
};
render(<BaseExample />);
- column render
- 列同时配置 render 与 renderType 时,render 优先级最高,覆盖内置 renderType 的单元格渲染(Table / TableView 一致)
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd)
const { Table, TableView } = _TablePage;
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>
<div>
<div style={{ marginBottom: 8, color: '#666' }}>Table</div>
<Table dataSource={dataSource} columns={columns} scroll={{ x: 1200 }} />
</div>
<div>
<div style={{ marginBottom: 8, color: '#666' }}>TableView</div>
<TableView dataSource={dataSource} columns={columns} />
</div>
</Flex>
);
};
render(<BaseExample />);
- renderMobile
- 移动端专用渲染:true 为默认卡片 List;function 完全接管;string 从 preset 按名称查找;支持 mobileSortToolbar 排序工具栏
- _TablePage(@kne/current-lib_table-page)[import * as _TablePage from "@kne/table-page"],(@kne/current-lib_table-page/dist/index.css),antd(antd)
const { Table, TableView, preset } = _TablePage;
const { Flex, Tag, Card, Button, Dropdown, Tabs, Checkbox } = 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: '客户名称', width: 220, 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: '订单金额',
width: 120,
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: '操作',
width: 140,
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 }) => {
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 }) => {
const status = statusMap[item.status] || { color: 'default', text: item.status };
const actionItems = getOrderActions(item);
const isSelected = checked;
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'
}}
>
<Checkbox 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 = ({ Component }) => {
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,不再渲染 antd Table;
开启 <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>
<Component
dataSource={dataSource}
columns={columns}
size="large"
controllerOpen={false}
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 ? (
