feat: 管理界面报刊风统一改造 — 移除独立暗色主题,预测/回测/监控/配置页增强

This commit is contained in:
shangfangjian
2026-09-18 10:42:14 +08:00
parent 91e406f5ee
commit cf0751d1ff
15 changed files with 1275 additions and 828 deletions
+128 -76
View File
@@ -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}`} />
}