feat: 管理界面报刊风统一改造 — 移除独立暗色主题,预测/回测/监控/配置页增强
This commit is contained in:
@@ -1,14 +1,9 @@
|
||||
/**
|
||||
* Admin 后台 - 通用 UI 组件集合
|
||||
* Admin 后台 - 通用 UI 组件集合(报刊风)
|
||||
*
|
||||
* 提供管理界面使用的所有基础组件:
|
||||
* - Card: 内容卡片
|
||||
* - StatCard: 统计数字卡片
|
||||
* - Badge: 状态标签
|
||||
* - DataTable: 通用数据表格
|
||||
* - ProgressBar: 进度条
|
||||
* - EmptyState: 空状态占位
|
||||
* - SectionHeader: 小节标题
|
||||
* 与前台共用同一套设计语言:
|
||||
* - 纸色底(paper)、墨色字(ink)、印报红唯一强调(press)
|
||||
* - 方正边框、细线分隔、宋体标题、无圆角、无彩色药丸标签
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
@@ -23,9 +18,7 @@ export function Card({
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={`rounded-lg border border-gray-800 bg-gray-900 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={`border border-ink-900 bg-paper-50 ${className}`}>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,14 +32,12 @@ export function CardHeader({
|
||||
action?: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="border-b border-gray-800 px-5 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-200">{title}</h3>
|
||||
<div className="border-b border-ink-900 bg-paper-100 px-4 py-2.5 sm:px-5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="font-serif text-sm font-bold text-ink-900">{title}</h3>
|
||||
{action}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="mt-1 text-xs text-gray-500">{description}</p>
|
||||
)}
|
||||
{description && <p className="mt-1 text-2xs text-ink-500">{description}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -58,7 +49,7 @@ export function CardBody({
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return <div className={`px-5 py-4 ${className}`}>{children}</div>
|
||||
return <div className={`px-4 py-4 sm:px-5 ${className}`}>{children}</div>
|
||||
}
|
||||
|
||||
// ── 统计卡片 ────────────────────────────────────────────────────
|
||||
@@ -66,53 +57,40 @@ export function CardBody({
|
||||
export function StatCard({
|
||||
label,
|
||||
value,
|
||||
icon,
|
||||
trend,
|
||||
hint,
|
||||
}: {
|
||||
label: string
|
||||
value: string | number
|
||||
icon?: string
|
||||
trend?: { value: number; positive: boolean }
|
||||
hint?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-900 px-5 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">{label}</span>
|
||||
{icon && (
|
||||
<span className="text-lg opacity-40" aria-hidden="true">
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
<div className="border border-ink-900 bg-paper-50 px-4 py-3.5">
|
||||
<span className="text-2xs tracking-[0.2em] text-ink-400">{label}</span>
|
||||
<div className="mt-1.5 font-serif text-3xl font-bold tabular-nums leading-none text-ink-900">
|
||||
{value}
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-semibold text-gray-100 tabular-nums">{value}</div>
|
||||
{trend && (
|
||||
<div
|
||||
className={`mt-1 text-xs tabular-nums ${
|
||||
trend.positive ? 'text-emerald-400' : 'text-red-400'
|
||||
}`}
|
||||
>
|
||||
{trend.positive ? '↑' : '↓'} {Math.abs(trend.value)}%
|
||||
</div>
|
||||
)}
|
||||
{hint && <div className="mt-1.5 text-2xs text-ink-400">{hint}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 状态标签 ────────────────────────────────────────────────────
|
||||
// ── 状态标记 ────────────────────────────────────────────────────
|
||||
// 报刊不用彩色药丸:小方块 + 文字,红=异常/失败,墨=正常,灰=中性
|
||||
|
||||
const BADGE_COLORS: Record<string, string> = {
|
||||
success: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30',
|
||||
running: 'bg-blue-500/15 text-blue-400 border-blue-500/30',
|
||||
queued: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30',
|
||||
failed: 'bg-red-500/15 text-red-400 border-red-500/30',
|
||||
pending: 'bg-gray-500/15 text-gray-400 border-gray-500/30',
|
||||
completed: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30',
|
||||
error: 'bg-red-500/15 text-red-400 border-red-500/30',
|
||||
warning: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30',
|
||||
info: 'bg-blue-500/15 text-blue-400 border-blue-500/30',
|
||||
win: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30',
|
||||
loss: 'bg-red-500/15 text-red-400 border-red-500/30',
|
||||
push: 'bg-gray-500/15 text-gray-400 border-gray-500/30',
|
||||
const MARK_STYLES: Record<string, { text: string; mark: string }> = {
|
||||
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({
|
||||
@@ -122,11 +100,10 @@ export function Badge({
|
||||
status: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
const colorClass = BADGE_COLORS[status] ?? 'bg-gray-500/15 text-gray-400 border-gray-500/30'
|
||||
const s = MARK_STYLES[status] ?? MARK_STYLES.pending
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${colorClass}`}
|
||||
>
|
||||
<span className={`inline-flex items-center gap-1.5 whitespace-nowrap text-2xs ${s.text}`}>
|
||||
<span className={`inline-block h-1.5 w-1.5 ${s.mark}`} aria-hidden="true" />
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
@@ -154,9 +131,9 @@ export function DataTable<T = any>({
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800 text-xs text-gray-500">
|
||||
<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.5 font-medium" style={{ width: col.width }}>
|
||||
<th key={col.key} className="px-3 py-2 font-medium" style={{ width: col.width }}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
@@ -166,10 +143,10 @@ export function DataTable<T = any>({
|
||||
{data.map(row => (
|
||||
<tr
|
||||
key={rowKey(row)}
|
||||
className="border-b border-gray-800/50 transition-colors hover:bg-gray-800/30"
|
||||
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-gray-300">
|
||||
<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
|
||||
@@ -185,26 +162,27 @@ export function DataTable<T = any>({
|
||||
)
|
||||
}
|
||||
|
||||
// ── 进度条 ──────────────────────────────────────────────────────
|
||||
// ── 进度条:同前台置信度细线 ────────────────────────────────────
|
||||
|
||||
export function ProgressBar({ value }: { value: number }) {
|
||||
const clamped = Math.max(0, Math.min(100, value))
|
||||
return (
|
||||
<div className="h-1.5 w-full rounded-full bg-gray-800" role="progressbar" aria-valuenow={clamped}>
|
||||
<div className="h-px w-full bg-ink-200" role="progressbar" aria-valuenow={clamped}>
|
||||
<div
|
||||
className="h-full rounded-full bg-blue-500 transition-all duration-300"
|
||||
className="h-px bg-press transition-[width] duration-500"
|
||||
style={{ width: `${clamped}%` }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 空状态 ──────────────────────────────────────────────────────
|
||||
// ── 空状态:同前台「本版暂无赛程」 ──────────────────────────────
|
||||
|
||||
export function EmptyState({ text = '暂无数据' }: { text?: string }) {
|
||||
export function EmptyState({ text = '暂无数据', sub }: { text?: string; sub?: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-gray-600">
|
||||
{text}
|
||||
<div className="border-y border-ink-200 py-12 text-center">
|
||||
<p className="font-serif text-sm text-ink-600">{text}</p>
|
||||
{sub && <p className="mt-1.5 text-xs text-ink-400">{sub}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -228,7 +206,7 @@ export function MobileCardList<T = any>({
|
||||
return (
|
||||
<div className="space-y-3 lg:hidden">
|
||||
{data.map((row, idx) => (
|
||||
<div key={idx} className="rounded-lg border border-gray-800 bg-gray-900 p-4">
|
||||
<div key={idx} className="border border-ink-900 bg-paper-50 p-4">
|
||||
{renderCard(row, idx)}
|
||||
</div>
|
||||
))}
|
||||
@@ -259,7 +237,7 @@ export function ResponsiveTable<T = any>({
|
||||
return (
|
||||
<>
|
||||
{/* 桌面端表格 */}
|
||||
<div className="hidden lg:block overflow-x-auto">
|
||||
<div className="hidden overflow-x-auto lg:block">
|
||||
<DataTable columns={columns} data={data} rowKey={rowKey} emptyText={emptyText} />
|
||||
</div>
|
||||
{/* 移动端卡片 */}
|
||||
@@ -268,7 +246,7 @@ export function ResponsiveTable<T = any>({
|
||||
)
|
||||
}
|
||||
|
||||
// ── 小节标题 ────────────────────────────────────────────────────
|
||||
// ── 小节标题:同前台 section-head ───────────────────────────────
|
||||
|
||||
export function SectionHeader({
|
||||
title,
|
||||
@@ -278,9 +256,83 @@ export function SectionHeader({
|
||||
description?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-100">{title}</h2>
|
||||
{description && <p className="mt-1 text-sm text-gray-500">{description}</p>}
|
||||
<div className="mb-5">
|
||||
<h2 className="section-head text-base">{title}</h2>
|
||||
{description && <p className="mt-1.5 text-xs text-ink-500">{description}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 提示条:错误红框(同前台) / 正常墨框 ────────────────────────
|
||||
|
||||
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 (
|
||||
<div className={`flex items-start justify-between gap-3 border px-4 py-3 ${style}`}>
|
||||
<div>
|
||||
<p className={`flex items-center gap-1.5 text-sm font-medium ${titleCls}`}>
|
||||
<span
|
||||
className={`inline-block h-1.5 w-1.5 ${kind === 'error' ? 'bg-press' : 'bg-ink-900'}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{title}
|
||||
</p>
|
||||
{message && (
|
||||
<p className="mt-0.5 whitespace-pre-wrap text-xs leading-relaxed text-ink-600">
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-ink-400 transition-colors hover:text-ink-900"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" className="h-4 w-4" fill="currentColor" aria-hidden="true">
|
||||
<path d="M6.3 5.3a1 1 0 011.4 0L10 7.6l2.3-2.3a1 1 0 111.4 1.4L11.4 9l2.3 2.3a1 1 0 01-1.4 1.4L10 10.4l-2.3 2.3a1 1 0 01-1.4-1.4L8.6 9 6.3 6.7a1 1 0 010-1.4z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 加载指示:同前台 Spinner ────────────────────────────────────
|
||||
|
||||
export function Spinner({ className = '' }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
className={`h-3.5 w-3.5 animate-spin ${className}`}
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.5" strokeOpacity="0.25" />
|
||||
<path d="M17.5 10A7.5 7.5 0 0010 2.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 骨架占位 ────────────────────────────────────────────────────
|
||||
|
||||
export function SkeletonBlock({ className = '' }: { className?: string }) {
|
||||
return <div className={`skeleton ${className}`} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user