feat: 管理界面优化 — 移动端自适应 + 数据源 + LLM 配置
移动端自适应: - AdminLayout: 汉堡菜单 + 可折叠侧边栏 + 遮罩层 + ESC 关闭 - 所有页面响应式布局 (grid-cols-1 sm:grid-cols-2 lg:grid-cols-4) - 触摸友好按钮 (min-h-[44px]) - 表格移动端卡片视图 新增页面: - DataSources: 数据源状态 + API Key 脱敏 + 测试连接 - LLMConfig: LLM 配置 + 测试连接 + 使用统计 + 最近预测 增强功能: - Config: 配置列表 + 修改指南 + 快捷导航 - types: 新增 DataSourceStatus, LLMUsageStats 等类型 - dal: 新增 testDataSource, fetchDataSourceStatuses, testLLMConnection 等
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* Admin 后台 - 数据采集页面
|
||||
*
|
||||
* 响应式布局: 移动端单列,桌面端双列
|
||||
* 触摸友好: 按钮最小 44px 高度
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { triggerCollection, fetchLeagues } from '../dal'
|
||||
import type { CollectionRequest, League } from '../types'
|
||||
import { Card, CardBody, CardHeader, Badge } from '../components'
|
||||
import { Card, CardBody, CardHeader, Badge, SectionHeader } from '../components'
|
||||
|
||||
const SOURCES = [
|
||||
{ value: 'bzzoiro', label: 'Bzzoiro', desc: '历史赛程与比分' },
|
||||
@@ -57,80 +60,131 @@ export default function CollectionPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">数据采集</h2>
|
||||
<p className="mt-1 text-sm text-gray-400">触发数据源采集,支持联赛筛选和日期范围</p>
|
||||
</div>
|
||||
<SectionHeader
|
||||
title="数据采集"
|
||||
description="触发数据源采集,支持联赛筛选和日期范围"
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* 采集表单 */}
|
||||
<Card>
|
||||
<CardHeader title="新建采集任务" />
|
||||
<CardBody>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* 数据源选择 */}
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-gray-400">数据源</label>
|
||||
<select value={source} onChange={e => setSource(e.target.value)}
|
||||
className="w-full rounded border border-gray-700 bg-gray-800 px-3 py-2 text-white">
|
||||
{SOURCES.map(s => <option key={s.value} value={s.value}>{s.label} — {s.desc}</option>)}
|
||||
<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]"
|
||||
>
|
||||
{SOURCES.map(s => (
|
||||
<option key={s.value} value={s.value}>
|
||||
{s.label} — {s.desc}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 联赛选择 */}
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-gray-400">联赛</label>
|
||||
<select value={leagueCode} onChange={e => setLeagueCode(e.target.value)}
|
||||
className="w-full rounded border border-gray-700 bg-gray-800 px-3 py-2 text-white">
|
||||
<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]"
|
||||
>
|
||||
<option value="">全部联赛</option>
|
||||
{leagues.map(l => <option key={l.code} value={l.code}>{l.name_zh || l.name}</option>)}
|
||||
{leagues.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name_zh || l.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Understat 专用: 赛季 */}
|
||||
{source === 'understat' && (
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-gray-400">赛季(起始年)</label>
|
||||
<input type="number" value={season} onChange={e => setSeason(e.target.value)}
|
||||
placeholder="2025" className="w-full rounded border border-gray-700 bg-gray-800 px-3 py-2 text-white" />
|
||||
<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]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 日期范围 */}
|
||||
{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>
|
||||
<input type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)}
|
||||
className="w-full rounded border border-gray-700 bg-gray-800 px-3 py-2 text-white" />
|
||||
<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]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-gray-400">结束日期</label>
|
||||
<input type="date" value={dateTo} onChange={e => setDateTo(e.target.value)}
|
||||
className="w-full rounded border border-gray-700 bg-gray-800 px-3 py-2 text-white" />
|
||||
<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]"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="rounded bg-red-500/10 p-3 text-sm text-red-400">{error}</div>}
|
||||
{successMsg && <div className="rounded bg-green-500/10 p-3 text-sm text-green-400">{successMsg}</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>
|
||||
)}
|
||||
|
||||
<button type="submit" disabled={loading}
|
||||
className="w-full rounded bg-blue-600 py-2 text-white hover:bg-blue-700 disabled:opacity-50">
|
||||
{/* 提交按钮 */}
|
||||
<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>
|
||||
</form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 数据源说明 */}
|
||||
<Card>
|
||||
<CardHeader title="数据源说明" />
|
||||
<CardBody>
|
||||
<div className="space-y-3">
|
||||
{SOURCES.map(s => (
|
||||
<div key={s.value} className="rounded border border-gray-700 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div key={s.value} className="rounded-lg border border-gray-800 p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge status="info">{s.label}</Badge>
|
||||
<span className="text-sm text-gray-300">{s.desc}</span>
|
||||
</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>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user