/** * Admin 后台 - 数据采集页面(报刊风) * * 响应式布局: 移动端单列,桌面端双列 */ import { useEffect, useState, useCallback } from 'react' import { triggerCollection, fetchLeagues } from '../dal' import type { CollectionRequest, League } from '../types' import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner } from '../components' const SOURCES = [ { value: 'bzzoiro', label: 'Bzzoiro', desc: '历史赛程与比分' }, { value: 'understat', label: 'Understat', desc: 'xG 进阶数据' }, { 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([]) const [source, setSource] = useState('bzzoiro') const [leagueCode, setLeagueCode] = useState('') const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') const [season, setSeason] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [result, setResult] = useState<{ title: string; detail: string } | null>(null) const loadLeagues = useCallback(async () => { const lg = await fetchLeagues() setLeagues(lg) }, []) useEffect(() => { loadLeagues() }, [loadLeagues]) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError(null) setResult(null) setLoading(true) try { const body: CollectionRequest = { source: source as CollectionRequest['source'], leagues: leagueCode ? [leagueCode] : undefined, league: leagueCode || undefined, season: season || undefined, date_from: dateFrom || undefined, date_to: dateTo || undefined, } const res = await triggerCollection(body) setResult(summarizeResult(res, source)) } catch (err: unknown) { setError(err instanceof Error ? err.message : '采集触发失败') } finally { setLoading(false) } } return (
{/* 采集表单 */}
{/* 数据源选择 */}
{/* 联赛选择 */}
{/* Understat 专用: 赛季 */} {source === 'understat' && (
setSeason(e.target.value)} placeholder="2025" className="field w-full" />
)} {/* 日期范围 */} {source !== 'injuries' && (
setDateFrom(e.target.value)} className="field w-full" />
setDateTo(e.target.value)} className="field w-full" />
)} {/* 消息提示 */} {error && setError(null)} />} {result && ( setResult(null)} /> )} {/* 提交按钮 */}
{/* 数据源说明 */}
{SOURCES.map(s => (
{s.label}

{s.desc}

))}

若后端配置了 ADMIN_API_KEY,采集接口需要管理员密钥。 遇到 401 请到「系统配置」页填写密钥。

) }