/** * Admin 后台 - 通用 UI 组件集合(报刊风) * * 与前台共用同一套设计语言: * - 纸色底(paper)、墨色字(ink)、印报红唯一强调(press) * - 方正边框、细线分隔、宋体标题、无圆角、无彩色药丸标签 */ import { ReactNode } from 'react' // ── 卡片 ──────────────────────────────────────────────────────── export function Card({ children, className = '', }: { children: ReactNode className?: string }) { return (
{children}
) } export function CardHeader({ title, description, action, }: { title: string description?: string action?: ReactNode }) { return (

{title}

{action}
{description &&

{description}

}
) } export function CardBody({ children, className = '', }: { children: ReactNode className?: string }) { return
{children}
} // ── 统计卡片 ──────────────────────────────────────────────────── export function StatCard({ label, value, hint, }: { label: string value: string | number hint?: string }) { return (
{label}
{value}
{hint &&
{hint}
}
) } // ── 状态标记 ──────────────────────────────────────────────────── // 报刊不用彩色药丸:小方块 + 文字,红=异常/失败,墨=正常,灰=中性 const MARK_STYLES: Record = { success: { text: 'text-ink-800', mark: 'bg-ink-900' }, completed: { text: 'text-ink-800', mark: 'bg-ink-900' }, win: { text: 'text-ink-800', mark: 'bg-ink-900' }, ok: { text: 'text-ink-800', mark: 'bg-ink-900' }, running: { text: 'text-ink-600', mark: 'bg-ink-400' }, info: { text: 'text-ink-600', mark: 'bg-ink-400' }, queued: { text: 'text-ink-500', mark: 'border border-ink-400' }, pending: { text: 'text-ink-400', mark: 'bg-ink-300' }, push: { text: 'text-ink-400', mark: 'bg-ink-300' }, warning: { text: 'text-press', mark: 'border border-press' }, failed: { text: 'text-press font-medium', mark: 'bg-press' }, error: { text: 'text-press font-medium', mark: 'bg-press' }, loss: { text: 'text-press font-medium', mark: 'bg-press' }, } export function Badge({ status, children, }: { status: string children: ReactNode }) { const s = MARK_STYLES[status] ?? MARK_STYLES.pending return ( ) } // ── 数据表格 ──────────────────────────────────────────────────── // eslint-disable-next-line @typescript-eslint/no-explicit-any export function DataTable({ 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 } return (
{columns.map(col => ( ))} {data.map(row => ( {columns.map(col => ( ))} ))}
{col.label}
{col.render ? col.render(row) : row != null && typeof row === 'object' && col.key in row ? String((row as Record)[col.key] ?? '—') : '—'}
) } // ── 进度条:同前台置信度细线 ──────────────────────────────────── export function ProgressBar({ value }: { value: number }) { const clamped = Math.max(0, Math.min(100, value)) return (
) } // ── 空状态:同前台「本版暂无赛程」 ────────────────────────────── export function EmptyState({ text = '暂无数据', sub }: { text?: string; sub?: string }) { return (

{text}

{sub &&

{sub}

}
) } // ── 移动端卡片列表 (替代桌面端表格) ──────────────────────────── // eslint-disable-next-line @typescript-eslint/no-explicit-any export function MobileCardList({ data, renderCard, emptyText = '暂无数据', }: { data: T[] renderCard: (row: T, index: number) => ReactNode emptyText?: string }) { if (data.length === 0) { return } return (
{data.map((row, idx) => (
{renderCard(row, idx)}
))}
) } // ── 响应式表格容器 ────────────────────────────────────────────── // eslint-disable-next-line @typescript-eslint/no-explicit-any export function ResponsiveTable({ 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 } return ( <> {/* 桌面端表格 */}
{/* 移动端卡片 */} ) } // ── 小节标题:同前台 section-head ─────────────────────────────── export function SectionHeader({ title, description, }: { title: string description?: string }) { return (

{title}

{description &&

{description}

}
) } // ── 提示条:错误红框(同前台) / 正常墨框 ──────────────────────── export function Alert({ kind, title, message, onClose, }: { kind: 'error' | 'ok' | 'info' title: string message?: string onClose?: () => void }) { const style = kind === 'error' ? 'border-press bg-press-wash' : kind === 'ok' ? 'border-ink-900 bg-paper-100' : 'border-ink-300 bg-paper-50' const titleCls = kind === 'error' ? 'text-press' : 'text-ink-900' return (

{message && (

{message}

)}
{onClose && ( )}
) } // ── 加载指示:同前台 Spinner ──────────────────────────────────── export function Spinner({ className = '' }: { className?: string }) { return ( ) } // ── 骨架占位 ──────────────────────────────────────────────────── export function SkeletonBlock({ className = '' }: { className?: string }) { return
}