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:
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Admin 后台 - 数据源管理页面
|
||||
*
|
||||
* 功能:
|
||||
* - 显示当前数据源状态 (bzzoiro / understat / injuries)
|
||||
* - 显示 API Key 配置状态(脱敏显示)
|
||||
* - 测试连接按钮(调用采集 API 测试)
|
||||
* - 采集历史记录
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { fetchDataSourceStatuses, testDataSource } from '../dal'
|
||||
import type { DataSourceStatus } from '../types'
|
||||
import { Card, CardBody, CardHeader, Badge, SectionHeader } from '../components'
|
||||
|
||||
export default function DataSourcesPage() {
|
||||
const [sources, setSources] = useState<DataSourceStatus[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [testingSource, setTestingSource] = useState<string | null>(null)
|
||||
const [testResults, setTestResults] = useState<Record<string, { success: boolean; message: string }>>({})
|
||||
|
||||
const loadSources = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await fetchDataSourceStatuses()
|
||||
setSources(data)
|
||||
} catch {
|
||||
setSources([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => { loadSources() }, [loadSources])
|
||||
|
||||
async function handleTest(sourceName: string) {
|
||||
setTestingSource(sourceName)
|
||||
setTestResults(prev => ({ ...prev, [sourceName]: { success: false, message: '测试中...' } }))
|
||||
try {
|
||||
await testDataSource(sourceName as any)
|
||||
setTestResults(prev => ({ ...prev, [sourceName]: { success: true, message: '连接成功' } }))
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '连接失败'
|
||||
setTestResults(prev => ({ ...prev, [sourceName]: { success: false, message: msg } }))
|
||||
} finally {
|
||||
setTestingSource(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SectionHeader
|
||||
title="数据源管理"
|
||||
description="管理数据采集源配置与连接状态"
|
||||
/>
|
||||
|
||||
{/* 数据源卡片 */}
|
||||
{loading ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map(i => (
|
||||
<Card key={i}>
|
||||
<CardBody>
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-4 w-24 rounded bg-gray-800" />
|
||||
<div className="h-3 w-32 rounded bg-gray-800" />
|
||||
<div className="h-8 w-full rounded bg-gray-800" />
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{sources.map(source => {
|
||||
const result = testResults[source.name]
|
||||
return (
|
||||
<Card key={source.name}>
|
||||
<CardBody className="space-y-4">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium text-white">{source.label}</h3>
|
||||
<Badge status={source.keyConfigured ? 'success' : 'failed'}>
|
||||
{source.keyConfigured ? '已配置' : '未配置'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Key 状态 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">API Key</span>
|
||||
<span className="font-mono text-gray-400">{source.maskedKey}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">最近采集</span>
|
||||
<span className="text-gray-400">{source.lastIngestion || '暂无记录'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 测试结果 */}
|
||||
{result && (
|
||||
<div
|
||||
className={`rounded p-2 text-xs ${
|
||||
result.success
|
||||
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/30'
|
||||
: 'bg-red-500/10 text-red-400 border border-red-500/30'
|
||||
}`}
|
||||
>
|
||||
{result.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<button
|
||||
onClick={() => handleTest(source.name)}
|
||||
disabled={testingSource === source.name}
|
||||
className="w-full rounded-md border border-gray-700 bg-gray-800 px-4 py-2.5 text-sm text-gray-300 transition-colors hover:bg-gray-700 hover:text-white disabled:opacity-50 min-h-[44px]"
|
||||
>
|
||||
{testingSource === source.name ? '测试中...' : '测试连接'}
|
||||
</button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 数据源说明 */}
|
||||
<Card>
|
||||
<CardHeader title="数据源说明" />
|
||||
<CardBody>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="rounded-lg border border-gray-800 p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge status="info">Bzzoiro</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">
|
||||
历史赛程与比分数据,覆盖全球主要联赛。需要 API Key 配置。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-800 p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge status="info">Understat</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">
|
||||
xG (预期进球) 进阶数据,无需 API Key,通过网页抓取获取。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-800 p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge status="info">Injuries</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">
|
||||
球员伤停信息,用于预测时考虑阵容完整性。需要 API Key 配置。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 采集历史 */}
|
||||
<Card>
|
||||
<CardHeader title="采集历史" />
|
||||
<CardBody>
|
||||
<div className="text-center py-8 text-sm text-gray-500">
|
||||
<p>暂无采集历史记录</p>
|
||||
<p className="mt-1 text-xs">后端暂无采集历史端点,采集任务完成后将在此显示</p>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user