feat: 前端 Error Boundary + 体验改进
- 新增 ErrorBoundary 组件: 捕获组件崩溃,显示友好错误页+重试按钮 - App: 用 ErrorBoundary 包裹,标题改为「先知 Profeto」 - Matches 页面: - 预测模式切换 (单次/多 Agent) - 加载中动画指示 - 预测中状态提示 - 错误提示可关闭
This commit is contained in:
@@ -73,6 +73,7 @@ export default function Matches() {
|
||||
const [predictingId, setPredictingId] = useState<number | null>(null)
|
||||
const [prediction, setPrediction] = useState<Prediction | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [mode, setMode] = useState<'single' | 'multi'>('multi')
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
@@ -100,7 +101,7 @@ export default function Matches() {
|
||||
const res = await fetch('/api/v1/predict', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ match_id: matchId }),
|
||||
body: JSON.stringify({ match_id: matchId, mode }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const t = await res.text()
|
||||
@@ -134,6 +135,17 @@ export default function Matches() {
|
||||
<option value="finished">已完赛</option>
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-gray-500">模式:</span>
|
||||
<button
|
||||
onClick={() => setMode('single')}
|
||||
className={`px-2 py-1 rounded text-xs ${mode === 'single' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600'}`}
|
||||
>单次</button>
|
||||
<button
|
||||
onClick={() => setMode('multi')}
|
||||
className={`px-2 py-1 rounded text-xs ${mode === 'multi' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600'}`}
|
||||
>多 Agent</button>
|
||||
</div>
|
||||
<button onClick={load} disabled={loading}
|
||||
className="bg-blue-600 text-white text-sm px-4 py-1.5 rounded hover:bg-blue-700 disabled:opacity-50">
|
||||
{loading ? '加载中...' : '刷新'}
|
||||
@@ -141,7 +153,12 @@ export default function Matches() {
|
||||
<span className="text-sm text-gray-500">共 {matches.length} 场</span>
|
||||
</div>
|
||||
|
||||
{error && <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded text-sm">{error}</div>}
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded text-sm flex items-center justify-between">
|
||||
<span>❌ {error}</span>
|
||||
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700 text-xs">✕</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 比赛表 */}
|
||||
<div className="bg-white rounded border overflow-hidden">
|
||||
@@ -157,7 +174,12 @@ export default function Matches() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{matches.length === 0 && !loading && (
|
||||
{loading && (
|
||||
<tr><td colSpan={6} className="text-center text-gray-400 py-8">
|
||||
<span className="inline-block animate-spin mr-2">⏳</span>加载中...
|
||||
</td></tr>
|
||||
)}
|
||||
{!loading && matches.length === 0 && (
|
||||
<tr><td colSpan={6} className="text-center text-gray-400 py-8">暂无数据,请先采集</td></tr>
|
||||
)}
|
||||
{matches.map(m => (
|
||||
@@ -189,6 +211,14 @@ export default function Matches() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 预测中指示 */}
|
||||
{predictingId && (
|
||||
<div className="bg-blue-50 border border-blue-200 text-blue-700 px-4 py-3 rounded text-sm flex items-center gap-2">
|
||||
<span className="inline-block animate-spin">⏳</span>
|
||||
LLM 预测中,请稍候...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 预测结果 */}
|
||||
{prediction && (
|
||||
<div className="bg-white rounded border p-5 space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user