---
title: 数据表格 DataTable
url: https://ui.zhaozimin.com/patterns/data-table/
markdown: https://ui.zhaozimin.com/patterns/data-table.md
group: 模式
origin: 提炼层组件（src/extracted），本仓库是唯一源头
source:
  - src/extracted/DataTable.jsx  # https://ui.zhaozimin.com/src/extracted/DataTable.jsx
  - src/extracted/extracted.css  # https://ui.zhaozimin.com/src/extracted/extracted.css
version: v0.2.0  # 设计系统版本，发布于 2026-09-23
scope: .zzm-v4   # 根节点必须带这个类，否则样式不生效
css: https://ui.zhaozimin.com/v/0.2.0/zzm.css   # 锁版本地址，本仓库再改也不影响你
---

# 数据表格 DataTable

> 同名但用途不同的数据先用一排小键分区，再进表格。浅灰 mono 表头、可排序列、细线分行；窄屏只横向滚动，不折行。

### 样张 · 分区 + 排序

点表头排序；再点一次反向

React：

```jsx
import { useMemo, useState } from 'react'
import { DataTable } from './extracted/DataTable.jsx'
import { StatusPill } from './extracted/Tag.jsx'
import { Btn } from './components/v4/Button.jsx'

const ROWS = [
  { code: 'G7KM4Q2HP9RX', from: '邀请人甲', to: '新学员 A', used: true, reward: 100 },
  { code: 'T2WQ8LN5VJ3C', from: '邀请人乙', to: '新学员 B', used: false, reward: 0 },
  { code: 'M9PD6RZ4KX1B', from: '邀请人丙', to: '新学员 C', used: true, reward: 200 },
]

const COLUMNS = [
  { key: 'code', label: '邀请码', fr: 1.2, mono: true },
  { key: 'from', label: '邀请人', fr: 1.1 },
  { key: 'to', label: '被邀请人', fr: 1.1 },
  { key: 'used', label: '状态', fr: 0.7, render: (r) => <StatusPill tone={r.used ? 'neutral' : 'ok'}>{r.used ? '已使用' : '可用'}</StatusPill> },
  { key: 'reward', label: '奖励', fr: 0.6, mono: true, accent: true, render: (r) => `¥${r.reward}` },
]

export default function DataTableDemo() {
  const [scope, setScope] = useState('register')
  const [sort, setSort] = useState({ key: 'from', dir: 'asc' })
  const rows = useMemo(() => {
    const out = [...ROWS].sort((a, b) => (a[sort.key] > b[sort.key] ? 1 : a[sort.key] < b[sort.key] ? -1 : 0))
    return sort.dir === 'desc' ? out.reverse() : out
  }, [sort])
  const onSort = (key) => setSort((s) => ({ key, dir: s.key === key && s.dir === 'asc' ? 'desc' : 'asc' }))
  return (
    <div style={{ width: '100%', display: 'grid', gap: 12 }}>
      <div style={{ display: 'flex', gap: 8 }}>
        <Btn size="sm" variant={scope === 'register' ? 'primary' : 'paper'} onClick={() => setScope('register')}>注册激活码</Btn>
        <Btn size="sm" variant={scope === 'course' ? 'primary' : 'paper'} onClick={() => setScope('course')}>课程 / 会员激活码</Btn>
      </div>
      <DataTable columns={COLUMNS} rows={rows} sort={sort} onSort={onSort} rowKey={(r) => r.code} />
    </div>
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<div style="width: 100%; display: grid; gap: 12px">
  <div style="display: flex; gap: 8px">
    <button type="button" class="zzm-btn zzm-btn--primary zzm-btn--sm">注册激活码</button>
    <button type="button" class="zzm-btn zzm-btn--sm">课程 / 会员激活码</button>
  </div>
  <div
    style="
      border: 1px solid rgba(0, 0, 0, 0.09);
      border-radius: 16px;
      overflow-x: auto;
      background: #ffffff;
    "
  >
    <div role="table" style="min-width: 680px">
      <div
        role="row"
        style="display:grid;grid-template-columns:1.2fr 1.1fr 1.1fr 0.7fr 0.6fr;gap:10px;padding:10px 18px;background:#FAFAF8;border-bottom:1px solid rgba(0,0,0,.07);font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:10px;letter-spacing:1px;color:#A2A29C"
      >
        <span role="columnheader">
          <button
            type="button"
            class="zzm-sort"
            style="
              padding: 0;
              border: 0;
              background: transparent;
              font: inherit;
              letter-spacing: inherit;
              color: inherit;
              cursor: pointer;
              border-radius: 4px;
            "
          >
            邀请码 ↕
          </button>
        </span>
        <span role="columnheader" aria-sort="ascending">
          <button
            type="button"
            class="zzm-sort"
            style="
              padding: 0;
              border: 0;
              background: transparent;
              font: inherit;
              letter-spacing: inherit;
              color: #3f3f3b;
              cursor: pointer;
              border-radius: 4px;
            "
          >
            邀请人 ↑
          </button>
        </span>
        <span role="columnheader">
          <button
            type="button"
            class="zzm-sort"
            style="
              padding: 0;
              border: 0;
              background: transparent;
              font: inherit;
              letter-spacing: inherit;
              color: inherit;
              cursor: pointer;
              border-radius: 4px;
            "
          >
            被邀请人 ↕
          </button>
        </span>
        <span role="columnheader">
          <button
            type="button"
            class="zzm-sort"
            style="
              padding: 0;
              border: 0;
              background: transparent;
              font: inherit;
              letter-spacing: inherit;
              color: inherit;
              cursor: pointer;
              border-radius: 4px;
            "
          >
            状态 ↕
          </button>
        </span>
        <span role="columnheader">
          <button
            type="button"
            class="zzm-sort"
            style="
              padding: 0;
              border: 0;
              background: transparent;
              font: inherit;
              letter-spacing: inherit;
              color: inherit;
              cursor: pointer;
              border-radius: 4px;
            "
          >
            奖励 ↕
          </button>
        </span>
      </div>
      <div
        role="row"
        style="
          display: grid;
          grid-template-columns: 1.2fr 1.1fr 1.1fr 0.7fr 0.6fr;
          gap: 10px;
          padding: 11px 18px;
          align-items: center;
          font-size: 12px;
          border-top: 0;
          color: #3f3f3b;
        "
      >
        <span
          role="cell"
          style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;letter-spacing:1px;min-width:0"
        >
          M9PD6RZ4KX1B
        </span>
        <span role="cell" style="min-width: 0">邀请人丙</span>
        <span role="cell" style="min-width: 0">新学员 C</span>
        <span role="cell" style="min-width: 0">
          <span
            style="
              display: inline-block;
              font-size: 10.5px;
              padding: 3px 8px;
              border-radius: 999px;
              background: #efefec;
              color: #8b8b85;
            "
          >
            已使用
          </span>
        </span>
        <span
          role="cell"
          style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;letter-spacing:1px;color:var(--zzm-accent, #A6402F);min-width:0"
        >
          ¥200
        </span>
      </div>
      <div
        role="row"
        style="
          display: grid;
          grid-template-columns: 1.2fr 1.1fr 1.1fr 0.7fr 0.6fr;
          gap: 10px;
          padding: 11px 18px;
          align-items: center;
          font-size: 12px;
          border-top: 1px solid rgba(0, 0, 0, 0.07);
          color: #3f3f3b;
        "
      >
        <span
          role="cell"
          style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;letter-spacing:1px;min-width:0"
        >
          T2WQ8LN5VJ3C
        </span>
        <span role="cell" style="min-width: 0">邀请人乙</span>
        <span role="cell" style="min-width: 0">新学员 B</span>
        <span role="cell" style="min-width: 0">
          <span
            style="
              display: inline-block;
              font-size: 10.5px;
              padding: 3px 8px;
              border-radius: 999px;
              background: rgba(79, 169, 106, 0.1);
              color: #3b8557;
            "
          >
            可用
          </span>
        </span>
        <span
          role="cell"
          style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;letter-spacing:1px;color:var(--zzm-accent, #A6402F);min-width:0"
        >
          ¥0
        </span>
      </div>
      <div
        role="row"
        style="
          display: grid;
          grid-template-columns: 1.2fr 1.1fr 1.1fr 0.7fr 0.6fr;
          gap: 10px;
          padding: 11px 18px;
          align-items: center;
          font-size: 12px;
          border-top: 1px solid rgba(0, 0, 0, 0.07);
          color: #3f3f3b;
        "
      >
        <span
          role="cell"
          style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;letter-spacing:1px;min-width:0"
        >
          G7KM4Q2HP9RX
        </span>
        <span role="cell" style="min-width: 0">邀请人甲</span>
        <span role="cell" style="min-width: 0">新学员 A</span>
        <span role="cell" style="min-width: 0">
          <span
            style="
              display: inline-block;
              font-size: 10.5px;
              padding: 3px 8px;
              border-radius: 999px;
              background: #efefec;
              color: #8b8b85;
            "
          >
            已使用
          </span>
        </span>
        <span
          role="cell"
          style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;letter-spacing:1px;color:var(--zzm-accent, #A6402F);min-width:0"
        >
          ¥100
        </span>
      </div>
    </div>
  </div>
</div>
```

### 样张 · 静态标记

React：

```jsx
import { DataTable } from './extracted/DataTable.jsx'
import { StatusPill } from './extracted/Tag.jsx'

export function Static() {
  return (
    <DataTable
      columns={[
        { key: 'code', label: '邀请码', fr: 1.2, mono: true },
        { key: 'from', label: '邀请人', fr: 1.1 },
        { key: 'state', label: '状态', fr: 0.7, render: (r) => <StatusPill>{r.state}</StatusPill> },
        { key: 'reward', label: '奖励', fr: 0.6, mono: true, accent: true },
      ]}
      rows={[{ code: 'G7KM4Q2HP9RX', from: '邀请人甲', state: '已使用', reward: '¥100' }]}
      minWidth={560}
    />
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<div
  style="
    border: 1px solid rgba(0, 0, 0, 0.09);
    border-radius: 16px;
    overflow-x: auto;
    background: #ffffff;
  "
>
  <div role="table" style="min-width: 560px">
    <div
      role="row"
      style="display:grid;grid-template-columns:1.2fr 1.1fr 0.7fr 0.6fr;gap:10px;padding:10px 18px;background:#FAFAF8;border-bottom:1px solid rgba(0,0,0,.07);font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:10px;letter-spacing:1px;color:#A2A29C"
    >
      <span role="columnheader">邀请码</span>
      <span role="columnheader">邀请人</span>
      <span role="columnheader">状态</span>
      <span role="columnheader">奖励</span>
    </div>
    <div
      role="row"
      style="
        display: grid;
        grid-template-columns: 1.2fr 1.1fr 0.7fr 0.6fr;
        gap: 10px;
        padding: 11px 18px;
        align-items: center;
        font-size: 12px;
        border-top: 0;
        color: #3f3f3b;
      "
    >
      <span
        role="cell"
        style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;letter-spacing:1px;min-width:0"
      >
        G7KM4Q2HP9RX
      </span>
      <span role="cell" style="min-width: 0">邀请人甲</span>
      <span role="cell" style="min-width: 0">
        <span
          style="
            display: inline-block;
            font-size: 10.5px;
            padding: 3px 8px;
            border-radius: 999px;
            background: #efefec;
            color: #8b8b85;
          "
        >
          已使用
        </span>
      </span>
      <span
        role="cell"
        style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;letter-spacing:1px;color:var(--zzm-accent, #A6402F);min-width:0"
      >
        ¥100
      </span>
    </div>
  </div>
</div>
```

## 属性

| 属性 | 取值 | 默认 | 说明 |
|---|---|---|---|
| `columns` | [{ key, label, fr?, mono?, accent?, render?, sortable? }] | [] | fr 为列宽份额；mono 用等宽字；accent 用朱色（金额） |
| `rows` | object[] | [] | 数据行 |
| `sort` | { key, dir: 'asc' · 'desc' } | — | 当前排序（受控） |
| `onSort` | (key) => void | — | 传了才显示排序键 |
| `minWidth` | number | 680 | 内容最小宽度，窄屏靠横向滚动 |
| `rowKey` | (row, i) => key | 下标 | 行的 key |

## 规格

表头：内距 10 × 18 · 底 `#FAFAF8` · mono 10 · 字距 1 · `#A2A29C` · 下方 .07 细线。数据行：内距 11 × 18 · 12px · 行间 .07 细线。容器：.09 描边 · 圆角 16。分区键：当前用 sm 墨键，其余 sm 纸键。

## 本页用到的 CSS

按样张里出现的类名，从源文件原样裁出（完整版见 zzm.css）：

```css
/* tokens.css · 用到的令牌 */
:root {
  --zzm-accent: #A6402F;
}

/* controls.css */

.zzm-v4 .zzm-btn {
  -webkit-appearance: none;
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 36px;
  padding: 0 16px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .2px;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  border-radius: 8px;                                   /* 圆角矩形——按钮禁胶囊 */
  border: 1px solid rgba(0, 0, 0, .14);
  color: #3F3F3B;
  background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .55),                 /* ④ 反光光晕环 */
    0 1px 2px rgba(0, 0, 0, .09),                       /* ③ 近影 */
    0 2px 6px rgba(0, 0, 0, .06),                       /* ③ 远影 */
    inset 0 1px 0 rgba(255, 255, 255, .92),             /* ② 顶部反光 */
    inset 0 -1px 0 rgba(0, 0, 0, .05);                  /* ① 底缘收沉 */
  transition: box-shadow .16s ease, transform .16s ease, background .16s ease,
    border-color .16s ease, filter .16s ease, opacity .16s ease;
}

.zzm-v4 .zzm-btn:hover {
  transform: translateY(-1px);
  border-color: rgba(0, 0, 0, .18);
  box-shadow:
    0 0 0 4px rgba(255, 255, 255, .8),
    0 2px 4px rgba(0, 0, 0, .10),
    0 4px 12px rgba(0, 0, 0, .09),
    inset 0 1px 0 rgba(255, 255, 255, .95),
    inset 0 -1px 0 rgba(0, 0, 0, .05);
}

.zzm-v4 .zzm-btn:active {
  transform: translateY(0);
  background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%);   /* ⑤ 凹面翻转 */
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .45),
    0 1px 0 rgba(255, 255, 255, .65),
    inset 0 2px 4px rgba(0, 0, 0, .13),
    inset 0 1px 2px rgba(0, 0, 0, .09);
}

.zzm-v4 .zzm-btn:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 3px rgba(166, 64, 47, .20),                   /* 键盘焦点＝朱色光晕 */
    0 1px 2px rgba(0, 0, 0, .09),
    0 2px 6px rgba(0, 0, 0, .06),
    inset 0 1px 0 rgba(255, 255, 255, .92);
}

.zzm-v4 .zzm-btn:disabled {
  cursor: not-allowed;
  transform: none;
}

.zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) {
  color: #A2A29C;
  background: #F2F2EF;
  border-color: rgba(0, 0, 0, .08);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04);
}

.zzm-v4 .zzm-btn--primary {
  color: #FFFFFF;
  border-color: rgba(0, 0, 0, .85);
  background: linear-gradient(180deg, #3A3A36 0%, #1D1D1B 58%, #141412 100%);
  box-shadow:
    0 0 0 3px rgba(22, 22, 22, .06),
    0 1px 2px rgba(0, 0, 0, .18),
    0 3px 10px rgba(22, 22, 22, .24),
    inset 0 1px 0 rgba(255, 255, 255, .20),
    inset 0 -1px 0 rgba(0, 0, 0, .38);
}

.zzm-v4 .zzm-btn--primary:hover {
  filter: brightness(1.12);
  border-color: rgba(0, 0, 0, .85);
  box-shadow:
    0 0 0 4px rgba(22, 22, 22, .09),
    0 2px 4px rgba(0, 0, 0, .20),
    0 5px 14px rgba(22, 22, 22, .28),
    inset 0 1px 0 rgba(255, 255, 255, .24),
    inset 0 -1px 0 rgba(0, 0, 0, .38);
}

.zzm-v4 .zzm-btn--primary:active {
  filter: none;
  background: linear-gradient(180deg, #101010 0%, #22221F 100%);
  box-shadow:
    0 0 0 3px rgba(22, 22, 22, .06),
    inset 0 2px 5px rgba(0, 0, 0, .55),
    inset 0 1px 2px rgba(0, 0, 0, .4);
}

.zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; }

.zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; }

/* extracted.css */

.zzm-v4 .zzm-sort { transition: color .16s ease; }

.zzm-v4 .zzm-sort:hover { color: #161616 !important; }

.zzm-v4 .zzm-switch:focus-visible,
.zzm-v4 .zzm-check:focus-visible,
.zzm-v4 .zzm-tab:focus-visible,
.zzm-v4 .zzm-page:focus-visible,
.zzm-v4 .zzm-sort:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(166, 64, 47, .2);
}
```

---

上一页：[侧栏导航](https://ui.zhaozimin.com/patterns/sidebar.md) · 下一页：[安全验证外壳](https://ui.zhaozimin.com/patterns/verify-shell.md) · 全站目录：https://ui.zhaozimin.com/llms.txt
