- 新建 components/ui/(Button/Input/Select/Modal/Tabs/Spinner/Skeleton 等),变体用联合类型约束,根治幻影变体 - admin/components.tsx 与 matches/ui.tsx 改为再导出壳,消除 Spinner 三份重复 - 日期工具收口到 lib/date.ts,滚动监听收口到 lib/useScroll.ts - index.css 引入 :root RGB 三元组令牌,tailwind.config 接 <alpha-value>(修复透明度修饰符静默失效) - 刘建毛草 5MB 全字库(jsDelivr 未锁版) → 1KB 自托管子集(仅「先知」二字,unicode-range 限定)
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
/**
|
|
* 数据表格族:DataTable / MobileCardList / ResponsiveTable。
|
|
* 原位于 admin/components.tsx。
|
|
*
|
|
* ResponsiveTable 按断点二选一渲染:桌面表格 / 移动卡片。
|
|
*/
|
|
import type { ReactNode } from 'react'
|
|
import { EmptyState } from './Feedback'
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function DataTable<T = any>({
|
|
columns,
|
|
data,
|
|
rowKey,
|
|
emptyText = '暂无数据',
|
|
}: {
|
|
columns: { key: string; label: string; render?: (row: T) => ReactNode; width?: string }[]
|
|
data: T[]
|
|
rowKey: (row: T) => string | number
|
|
emptyText?: string
|
|
}) {
|
|
if (data.length === 0) {
|
|
return <EmptyState text={emptyText} />
|
|
}
|
|
|
|
return (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left text-sm">
|
|
<thead>
|
|
<tr className="border-b border-ink-900 text-2xs tracking-wider text-ink-500">
|
|
{columns.map(col => (
|
|
<th key={col.key} className="px-3 py-2 font-medium" style={{ width: col.width }}>
|
|
{col.label}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.map(row => (
|
|
<tr
|
|
key={rowKey(row)}
|
|
className="border-b border-ink-200 transition-colors hover:bg-paper-100"
|
|
>
|
|
{columns.map(col => (
|
|
<td key={col.key} className="px-3 py-2.5 text-ink-800">
|
|
{col.render
|
|
? col.render(row)
|
|
: row != null && typeof row === 'object' && col.key in row
|
|
? String((row as Record<string, unknown>)[col.key] ?? '—')
|
|
: '—'}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ── 移动端卡片列表 (替代桌面端表格) ────────────────────────────
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function MobileCardList<T = any>({
|
|
data,
|
|
renderCard,
|
|
emptyText = '暂无数据',
|
|
}: {
|
|
data: T[]
|
|
renderCard: (row: T, index: number) => ReactNode
|
|
emptyText?: string
|
|
}) {
|
|
if (data.length === 0) {
|
|
return <EmptyState text={emptyText} />
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3 lg:hidden">
|
|
{data.map((row, idx) => (
|
|
<div key={idx} className="border border-ink-900 bg-paper-50 p-4">
|
|
{renderCard(row, idx)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ── 响应式表格容器 ──────────────────────────────────────────────
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function ResponsiveTable<T = any>({
|
|
columns,
|
|
data,
|
|
rowKey,
|
|
cardRender,
|
|
emptyText = '暂无数据',
|
|
}: {
|
|
columns: { key: string; label: string; render?: (row: T) => ReactNode; width?: string }[]
|
|
data: T[]
|
|
rowKey: (row: T) => string | number
|
|
cardRender: (row: T, index: number) => ReactNode
|
|
emptyText?: string
|
|
}) {
|
|
if (data.length === 0) {
|
|
return <EmptyState text={emptyText} />
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* 桌面端表格 */}
|
|
<div className="hidden overflow-x-auto lg:block">
|
|
<DataTable columns={columns} data={data} rowKey={rowKey} emptyText={emptyText} />
|
|
</div>
|
|
{/* 移动端卡片 */}
|
|
<MobileCardList data={data} renderCard={cardRender} emptyText={emptyText} />
|
|
</>
|
|
)
|
|
}
|