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
+58 -41
View File
@@ -1,14 +1,13 @@
/**
* Admin 后台 - 数据采集页面
* Admin 后台 - 数据采集页面(报刊风)
*
* 响应式布局: 移动端单列,桌面端双列
* 触摸友好: 按钮最小 44px 高度
*/
import { useEffect, useState, useCallback } from 'react'
import { triggerCollection, fetchLeagues } from '../dal'
import type { CollectionRequest, League } from '../types'
import { Card, CardBody, CardHeader, Badge, SectionHeader } from '../components'
import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner } from '../components'
const SOURCES = [
{ value: 'bzzoiro', label: 'Bzzoiro', desc: '历史赛程与比分' },
@@ -16,6 +15,31 @@ const SOURCES = [
{ value: 'injuries', label: 'Injuries', desc: '球员伤停' },
] as const
/** 把采集接口返回摘要成一两行可读文字 */
function summarizeResult(res: any, source: string): { title: string; detail: string } {
if (res && typeof res === 'object') {
if (source === 'bzzoiro' && ('total_inserted' in res || 'total_updated' in res)) {
return {
title: `采集完成:新增 ${res.total_inserted ?? 0} 条,更新 ${res.total_updated ?? 0}`,
detail: Array.isArray(res.errors) && res.errors.length > 0 ? res.errors.join('\n') : '',
}
}
if ('count' in res || 'updated' in res) {
const parts = [
`新增 ${res.count ?? 0}`,
`更新 ${res.updated ?? 0}`,
`跳过 ${res.skipped ?? 0}`,
`未匹配 ${res.unmatched ?? 0}`,
]
return {
title: `采集完成:${parts.join(' / ')}`,
detail: Array.isArray(res.errors) && res.errors.length > 0 ? res.errors.join('\n') : '',
}
}
}
return { title: '采集完成', detail: JSON.stringify(res)?.slice(0, 300) ?? '' }
}
export default function CollectionPage() {
const [leagues, setLeagues] = useState<League[]>([])
const [source, setSource] = useState<string>('bzzoiro')
@@ -25,7 +49,7 @@ export default function CollectionPage() {
const [season, setSeason] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [successMsg, setSuccessMsg] = useState<string | null>(null)
const [result, setResult] = useState<{ title: string; detail: string } | null>(null)
const loadLeagues = useCallback(async () => {
const lg = await fetchLeagues()
@@ -37,7 +61,7 @@ export default function CollectionPage() {
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError(null)
setSuccessMsg(null)
setResult(null)
setLoading(true)
try {
@@ -50,7 +74,7 @@ export default function CollectionPage() {
date_to: dateTo || undefined,
}
const res = await triggerCollection(body)
setSuccessMsg(`采集完成: ${JSON.stringify(res).slice(0, 200)}`)
setResult(summarizeResult(res, source))
} catch (err: unknown) {
setError(err instanceof Error ? err.message : '采集触发失败')
} finally {
@@ -62,7 +86,7 @@ export default function CollectionPage() {
<div className="space-y-6">
<SectionHeader
title="数据采集"
description="触发数据源采集,支持联赛筛选和日期范围"
description="触发数据源采集,支持联赛筛选和日期范围。采集为同步执行,大范围日期耗时较长。"
/>
<div className="grid gap-6 lg:grid-cols-2">
@@ -73,11 +97,11 @@ export default function CollectionPage() {
<form onSubmit={handleSubmit} className="space-y-4">
{/* 数据源选择 */}
<div>
<label className="mb-1.5 block text-xs font-medium text-gray-400"></label>
<label className="mb-1.5 block text-xs text-ink-500"></label>
<select
value={source}
onChange={e => setSource(e.target.value)}
className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2.5 text-white min-h-[44px]"
className="field w-full"
>
{SOURCES.map(s => (
<option key={s.value} value={s.value}>
@@ -89,11 +113,11 @@ export default function CollectionPage() {
{/* 联赛选择 */}
<div>
<label className="mb-1.5 block text-xs font-medium text-gray-400"></label>
<label className="mb-1.5 block text-xs text-ink-500"></label>
<select
value={leagueCode}
onChange={e => setLeagueCode(e.target.value)}
className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2.5 text-white min-h-[44px]"
className="field w-full"
>
<option value=""></option>
{leagues.map(l => (
@@ -105,13 +129,13 @@ export default function CollectionPage() {
{/* Understat 专用: 赛季 */}
{source === 'understat' && (
<div>
<label className="mb-1.5 block text-xs font-medium text-gray-400">()</label>
<label className="mb-1.5 block text-xs text-ink-500">()</label>
<input
type="number"
value={season}
onChange={e => setSeason(e.target.value)}
placeholder="2025"
className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2.5 text-white min-h-[44px]"
className="field w-full"
/>
</div>
)}
@@ -120,45 +144,40 @@ export default function CollectionPage() {
{source !== 'injuries' && (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-xs font-medium text-gray-400"></label>
<label className="mb-1.5 block text-xs text-ink-500"></label>
<input
type="date"
value={dateFrom}
onChange={e => setDateFrom(e.target.value)}
className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2.5 text-white min-h-[44px]"
className="field w-full"
/>
</div>
<div>
<label className="mb-1.5 block text-xs font-medium text-gray-400"></label>
<label className="mb-1.5 block text-xs text-ink-500"></label>
<input
type="date"
value={dateTo}
onChange={e => setDateTo(e.target.value)}
className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2.5 text-white min-h-[44px]"
className="field w-full"
/>
</div>
</div>
)}
{/* 消息提示 */}
{error && (
<div className="rounded-md bg-red-500/10 p-3 text-sm text-red-400 border border-red-500/30">
{error}
</div>
)}
{successMsg && (
<div className="rounded-md bg-emerald-500/10 p-3 text-sm text-emerald-400 border border-emerald-500/30">
{successMsg}
</div>
{error && <Alert kind="error" title="采集失败" message={error} onClose={() => setError(null)} />}
{result && (
<Alert
kind="ok"
title={result.title}
message={result.detail || undefined}
onClose={() => setResult(null)}
/>
)}
{/* 提交按钮 */}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-blue-600 px-4 py-2.5 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors min-h-[44px]"
>
{loading ? '采集中...' : '触发采集'}
<button type="submit" disabled={loading} className="btn btn-solid w-full">
{loading ? (<><Spinner /> </>) : '触发采集'}
</button>
</form>
</CardBody>
@@ -170,21 +189,19 @@ export default function CollectionPage() {
<CardBody>
<div className="space-y-3">
{SOURCES.map(s => (
<div key={s.value} className="rounded-lg border border-gray-800 p-4">
<div className="flex items-center gap-2 mb-2">
<div key={s.value} className="border-b border-ink-200 px-1 py-3 last:border-b-0">
<div className="flex items-center gap-3">
<Badge status="info">{s.label}</Badge>
<p className="text-xs text-ink-600">{s.desc}</p>
</div>
<p className="text-sm text-gray-400">{s.desc}</p>
</div>
))}
</div>
{/* 移动端提示 */}
<div className="mt-4 rounded-lg border border-blue-500/30 bg-blue-500/5 p-3">
<p className="text-xs text-blue-300">
💡 ,,
</p>
</div>
<p className="mt-4 border-l-2 border-ink-300 pl-3 text-2xs leading-relaxed text-ink-500">
ADMIN_API_KEY,
401
</p>
</CardBody>
</Card>
</div>