模式 · PATTERN
数据表格 DataTable
同名但用途不同的数据先用一排小键分区,再进表格。浅灰 mono 表头、可排序列、细线分行;窄屏只横向滚动,不折行。
M9PD6RZ4KX1B邀请人丙新学员 C已使用¥200
T2WQ8LN5VJ3C邀请人乙新学员 B可用¥0
G7KM4Q2HP9RX邀请人甲新学员 A已使用¥100
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>
)
}<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,'SF Mono',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,'SF Mono',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,'SF Mono',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,'SF Mono',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,'SF Mono',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,'SF Mono',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,'SF Mono',Menlo,monospace;letter-spacing:1px;color:var(--zzm-accent, #A6402F);min-width:0"
>
¥100
</span>
</div>
</div>
</div>
</div>/* 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);
}邀请码邀请人状态奖励
G7KM4Q2HP9RX邀请人甲已使用¥100
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}
/>
)
}<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,'SF Mono',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,'SF Mono',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,'SF Mono',Menlo,monospace;letter-spacing:1px;color:var(--zzm-accent, #A6402F);min-width:0"
>
¥100
</span>
</div>
</div>
</div>/* tokens.css · 用到的令牌 */
:root {
--zzm-accent: #A6402F;
}属性
| 属性 | 取值 | 默认 | 说明 |
|---|---|---|---|
| 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 纸键。