feat: 前端 Error Boundary + 体验改进

- 新增 ErrorBoundary 组件: 捕获组件崩溃,显示友好错误页+重试按钮
- App: 用 ErrorBoundary 包裹,标题改为「先知 Profeto」
- Matches 页面:
  - 预测模式切换 (单次/多 Agent)
  - 加载中动画指示
  - 预测中状态提示
  - 错误提示可关闭
This commit is contained in:
shangfangjian
2026-09-09 21:58:57 +08:00
parent 494751bbb0
commit 3071ffd533
4 changed files with 2825 additions and 12 deletions
+33 -3
View File
@@ -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">