feat: 管理界面报刊风统一改造 — 移除独立暗色主题,预测/回测/监控/配置页增强
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user