npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 配置,以及 selectedRowKeysselectedRowsclearSelectedRows 等状态管理能力。

useSort

排序 Hook,配合 TableViewsortRender 实现表头排序交互。支持单列排序与多列排序,排序状态循环切换:DESC → ASC → 取消。提供 sortDataSource 工具函数用于本地排序(包含中文排序)。

渲染逻辑

桌面端:CSS Grid 表格

TableView 默认以 CSS Grid 渲染表头与表体。列配置经 resolveColumns 解析后,按 display !== false 过滤得到布局列,再计算 gridTemplateColumns(支持 span 栅格、width 固定宽、列宽拖动后的 colsSize)。每行数据走统一的单元格渲染管线:

  1. computeColumnsValue:按列取原始值(getValueOfdataSource[name]),经 format 格式化,过滤 display 为 false 或空值不展示的行
  2. computeDisplay:空值走 placeholder / renderPlaceholder;非空值调用列 render(见下方优先级)
  3. renderCellContent:按列 ellipsis / cellFullWidth 包裹 Ellipsis 与宽度约束后输出

自定义外层布局时,可通过 render={({ header, renderBody }) => ...} 拆分表头与表体;无数据时仍渲染表头并展示 empty

列渲染优先级

| 优先级 | 来源 | 说明 | | ------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | 1 | column.render | 自定义函数,最高优先级;存在时不再走 renderType 内置渲染 | | 2 | renderType | 声明式类型(mainamounttagstatustagListlistoptionsdescription 等),由 resolveColumn 注入对应 render 与列宽维度 | | 3 | 原始值 | 无 render 且无有效 renderType 时,直接展示格式化后的 value |

renderrenderType 可同时配置:renderType 仍负责注入 width / min / max / ellipsis 等布局维度,仅单元格内容由 render 覆盖。

renderType 支持尺寸修饰符组合,如 enum-smallmain-largeshort / small / large),维度可通过 globalParams.renderTypeSize 全局覆盖。

移动端:renderMobile

移动端判断统一使用 @kne/responsive-utilsuseIsMobile()(断点 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 列设置 mobileOptionsbuttonGroup.showLength: 0,仅保留「⋯」入口,避免按钮截断
  • 支持 rowSelection(左侧 checkbox / radio);allowSelectedAll 时列表顶部显示全选
  • 卡片 padding 跟随 size,复用 --kne-table-cell-padding CSS 变量

列渲染类型系统

通过 renderType 属性,可以用声明式的方式定义列的渲染样式,无需手写 render 函数。内置 mainamounttagstatustagListlistoptionsdescription 等类型,并支持与 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 ? &#96;${selectedOrder.id} (${selectedOrder.customerName})&#96; : '无'}</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: &#96;1px solid ${checked ? 'var(--primary-color-2, #91caff)' : isChild ? '#e8e8e8' : '#f0f0f0'}&#96;,
        borderLeft: &#96;${isChild ? level * 18 : 1}px solid ${checked ? 'var(--primary-color, #1677ff)' : isChild ? '#bfbfbf' : '#f0f0f0'}&#96;,
        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: &#96;1px solid ${isChild ? '#ebebeb' : '#f0f0f0'}&#96;
        }}
      >
        <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 ? &#96;子级 L${level}&#96; : '根节点'}
        </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: &#96;确定删除 ${item.id} 吗?&#96;,
          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: &#96;确定删除 ${item.id} 吗?&#96;, 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>{&#96;
            .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;
            }
          &#96;}</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: &#96;1px solid ${isSelected ? 'var(--primary-color-2, var(--primary-color, #1677ff))' : 'transparent'}&#96;,
        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(&#96;已批量发货 ${selectedRowKeys.length} 个订单&#96;);
          clearSelectedRows();
        }}
        onBatchExport={() => message.info(&#96;正在导出 ${selectedRowKeys.length} 个订单&#96;)}
      />
      <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: &apos;radio&apos;</code> — 选择列在表格最左侧
      </div>
      <div style={{ marginBottom: 12, padding: '12px', background: '#f5f5f5', borderRadius: 8 }}>
        当前选中:{selectedOrder ? &#96;${selectedOrder.id}(${selectedOrder.customerName})&#96; : '无'}
      </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 说明