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 { useState } from 'react'
|
||||
import { triggerBacktest, fetchEvalSummary } from '../dal'
|
||||
import type { BacktestRequest, EvalSummary } from '../types'
|
||||
import { Card, CardBody, CardHeader, Badge } from '../components'
|
||||
import { Card, CardBody, CardHeader, Badge, SectionHeader } from '../components'
|
||||
|
||||
export default function BacktestPage() {
|
||||
const [leagueId, setLeagueId] = useState('')
|
||||
@@ -47,72 +50,104 @@ export default function BacktestPage() {
|
||||
|
||||
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={handleBacktest} className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-gray-400">联赛 ID (可选)</label>
|
||||
<input type="number" value={leagueId} onChange={e => setLeagueId(e.target.value)}
|
||||
placeholder="留空=全部" className="w-full rounded border border-gray-700 bg-gray-800 px-3 py-2 text-white" />
|
||||
<input
|
||||
type="number"
|
||||
value={leagueId}
|
||||
onChange={e => setLeagueId(e.target.value)}
|
||||
placeholder="留空=全部"
|
||||
className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2.5 text-white min-h-[44px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-gray-400">场数限制</label>
|
||||
<input type="number" value={limit} onChange={e => setLimit(parseInt(e.target.value) || 20)}
|
||||
className="w-full rounded border border-gray-700 bg-gray-800 px-3 py-2 text-white" />
|
||||
<input
|
||||
type="number"
|
||||
value={limit}
|
||||
onChange={e => setLimit(parseInt(e.target.value) || 20)}
|
||||
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>
|
||||
<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="single">单次调用 (快)</option>
|
||||
<option value="multi">多 Agent (慢)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && <div className="rounded bg-red-500/10 p-3 text-sm text-red-400">{error}</div>}
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-500/10 p-3 text-sm text-red-400 border border-red-500/30">
|
||||
{error}
|
||||
</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>
|
||||
|
||||
{/* 结果区域 */}
|
||||
<div className="space-y-6">
|
||||
{result && (
|
||||
<Card>
|
||||
<CardHeader title="回测结果" />
|
||||
<CardBody>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded bg-gray-800 p-3 text-center">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-lg bg-gray-800 p-4 text-center">
|
||||
<div className="text-2xl font-bold text-white">{result.scored}/{result.total}</div>
|
||||
<div className="text-xs text-gray-400">已评分</div>
|
||||
<div className="text-xs text-gray-400 mt-1">已评分</div>
|
||||
</div>
|
||||
<div className="rounded bg-gray-800 p-3 text-center">
|
||||
<div className="rounded-lg bg-gray-800 p-4 text-center">
|
||||
<div className="text-2xl font-bold text-blue-400">
|
||||
{result.accuracy_1x2?.toFixed(1) ?? '—'}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">1X2 准确率</div>
|
||||
<div className="text-xs text-gray-400 mt-1">1X2 准确率</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
@@ -120,21 +155,40 @@ export default function BacktestPage() {
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader title="模型评估" action={<button onClick={loadEval} className="text-xs text-blue-400 hover:underline">刷新</button>} />
|
||||
<CardHeader
|
||||
title="模型评估"
|
||||
action={
|
||||
<button
|
||||
onClick={loadEval}
|
||||
className="rounded px-2 py-1 text-xs text-blue-400 hover:bg-gray-800 min-h-[44px] min-w-[44px]"
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<CardBody>
|
||||
{!evalSummary ? (
|
||||
<p className="text-gray-400">点击"刷新"加载评估数据</p>
|
||||
<div className="text-center py-8 text-sm text-gray-500">
|
||||
<p>点击"刷新"加载评估数据</p>
|
||||
</div>
|
||||
) : evalSummary.summary?.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{evalSummary.summary.map((s: any, i: number) => (
|
||||
<div key={i} className="flex items-center justify-between rounded border border-gray-700 p-2">
|
||||
<div
|
||||
key={i}
|
||||
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">{s.provider}/{s.model}</span>
|
||||
<Badge status="info">{s.accuracy?.toFixed(1)}% ({s.correct}/{s.total})</Badge>
|
||||
<Badge status="info">
|
||||
{s.accuracy?.toFixed(1)}% ({s.correct}/{s.total})
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-400">暂无评估数据</p>
|
||||
<div className="text-center py-8 text-sm text-gray-500">
|
||||
<p>暂无评估数据</p>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user