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:
shangfangjian
2026-09-17 02:43:44 +08:00
parent 6680da7d61
commit 91e406f5ee
14 changed files with 1312 additions and 174 deletions
+174
View File
@@ -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>
)
}