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
+43 -15
View File
@@ -1,11 +1,14 @@
/**
* Admin 后台 - 预测管理页面
*
* 响应式布局: 移动端单列,桌面端双列
* 触摸友好: 按钮最小 44px 高度
*/
import { useEffect, useState } from 'react'
import { triggerPrediction, fetchPredictions, fetchMatches } from '../dal'
import type { Match, Prediction } from '../types'
import { Card, CardBody, CardHeader, Badge } from '../components'
import { Card, CardBody, CardHeader, Badge, SectionHeader } from '../components'
export default function PredictionsPage() {
const [matches, setMatches] = useState<Match[]>([])
@@ -40,20 +43,24 @@ export default function PredictionsPage() {
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"> LLM </p>
</div>
<SectionHeader
title="预测管理"
description="触发 LLM 足球预测"
/>
<div className="grid gap-6 lg:grid-cols-2">
{/* 新建预测 */}
<Card>
<CardHeader title="新建预测" />
<CardBody>
<form onSubmit={handlePredict} className="space-y-4">
<div>
<label className="mb-1.5 block text-xs font-medium text-gray-400"></label>
<select value={matchId} onChange={e => setMatchId(e.target.value)}
className="w-full rounded border border-gray-700 bg-gray-800 px-3 py-2 text-white">
<select
value={matchId}
onChange={e => setMatchId(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>
{matches.map(m => (
<option key={m.id} value={m.id}>
@@ -65,33 +72,54 @@ export default function PredictionsPage() {
<div>
<label className="mb-1.5 block text-xs font-medium text-gray-400"></label>
<select value={mode} onChange={e => setMode(e.target.value as any)}
className="w-full rounded border border-gray-700 bg-gray-800 px-3 py-2 text-white">
<select
value={mode}
onChange={e => setMode(e.target.value as 'single' | 'multi')}
className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2.5 text-white min-h-[44px]"
>
<option value="multi"> Agent (5 + )</option>
<option value="single"></option>
</select>
</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 || !matchId}
className="w-full rounded bg-blue-600 py-2 text-white hover:bg-blue-700 disabled:opacity-50">
<button
type="submit"
disabled={loading || !matchId}
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>
{predictions.length === 0 ? (
<p className="text-gray-400"></p>
<div className="text-center py-8 text-sm text-gray-500">
<p></p>
<p className="mt-1 text-xs"></p>
</div>
) : (
<div className="space-y-2">
{predictions.slice(0, 10).map(p => (
<div key={p.id} className="flex items-center justify-between rounded border border-gray-700 p-2">
<div
key={p.id}
className="flex flex-col sm:flex-row sm:items-center justify-between rounded-lg border border-gray-800 p-3 gap-2"
>
<span className="text-sm text-gray-300">
Match #{p.match_id} · {p.model}
</span>