Skip to content

Commit 0dcbd48

Browse files
author
CodeJudge
committed
feat: 错误边界 + 快捷键帮助 + 提交时间可视化
1 parent 085fb90 commit 0dcbd48

5 files changed

Lines changed: 190 additions & 9 deletions

File tree

frontend/src/App.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Routes, Route } from 'react-router-dom';
22
import React, { Suspense, lazy } from 'react';
33
import Navbar from './components/Navbar';
44
import Footer from './components/Footer';
5+
import ErrorBoundary from './components/ErrorBoundary';
6+
import KeyboardShortcuts from './components/KeyboardShortcuts';
57

68
const Home = lazy(() => import('./pages/Home'));
79
const Login = lazy(() => import('./pages/Login'));
@@ -20,7 +22,8 @@ export default function App() {
2022
<div className="min-h-screen bg-dark-950">
2123
<Navbar />
2224
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
23-
<Suspense fallback={
25+
<ErrorBoundary>
26+
<Suspense fallback={
2427
<div className="flex items-center justify-center min-h-[60vh]">
2528
<div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin" />
2629
</div>
@@ -40,8 +43,10 @@ export default function App() {
4043
<Route path="*" element={<NotFound />} />
4144
</Routes>
4245
</Suspense>
46+
</ErrorBoundary>
4347
<Footer />
4448
</main>
49+
<KeyboardShortcuts />
4550
</div>
4651
);
4752
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
import { AlertTriangle, RefreshCw } from 'lucide-react';
3+
4+
interface Props {
5+
children: React.ReactNode;
6+
}
7+
8+
interface State {
9+
hasError: boolean;
10+
error: Error | null;
11+
}
12+
13+
export default class ErrorBoundary extends React.Component<Props, State> {
14+
constructor(props: Props) {
15+
super(props);
16+
this.state = { hasError: false, error: null };
17+
}
18+
19+
static getDerivedStateFromError(error: Error) {
20+
return { hasError: true, error };
21+
}
22+
23+
componentDidCatch(error: Error, info: React.ErrorInfo) {
24+
console.error('ErrorBoundary caught:', error, info);
25+
}
26+
27+
render() {
28+
if (this.state.hasError) {
29+
return (
30+
<div className="flex items-center justify-center min-h-[40vh]">
31+
<div className="card p-10 text-center max-w-md">
32+
<AlertTriangle className="w-12 h-12 text-red-400 mx-auto mb-4" />
33+
<h2 className="text-xl font-semibold text-white mb-2">页面出现错误</h2>
34+
<p className="text-dark-400 text-sm mb-4">
35+
{this.state.error?.message || '发生了未知错误'}
36+
</p>
37+
<button
38+
onClick={() => {
39+
this.setState({ hasError: false, error: null });
40+
window.location.reload();
41+
}}
42+
className="btn-primary inline-flex items-center gap-2"
43+
>
44+
<RefreshCw className="w-4 h-4" />
45+
重新加载
46+
</button>
47+
</div>
48+
</div>
49+
);
50+
}
51+
52+
return this.props.children;
53+
}
54+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useState, useEffect } from 'react';
2+
import { Keyboard, X } from 'lucide-react';
3+
4+
const SHORTCUTS = [
5+
{ keys: '/', desc: '跳转到搜索 / 题库' },
6+
{ keys: 'Ctrl+Enter', desc: '提交答案' },
7+
{ keys: '?', desc: '打开快捷键帮助' },
8+
];
9+
10+
export default function KeyboardShortcuts() {
11+
const [open, setOpen] = useState(false);
12+
13+
useEffect(() => {
14+
const handler = (e: KeyboardEvent) => {
15+
if (e.key === '?' && !e.ctrlKey && !e.metaKey && !['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) {
16+
e.preventDefault();
17+
setOpen(o => !o);
18+
}
19+
if (e.key === 'Escape') setOpen(false);
20+
};
21+
window.addEventListener('keydown', handler);
22+
return () => window.removeEventListener('keydown', handler);
23+
}, []);
24+
25+
if (!open) return null;
26+
27+
return (
28+
<div className="fixed inset-0 z-[100] flex items-center justify-center" onClick={() => setOpen(false)}>
29+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
30+
<div className="relative bg-dark-800 border border-dark-600 rounded-xl p-6 max-w-sm w-full mx-4 shadow-2xl" onClick={e => e.stopPropagation()}>
31+
<div className="flex items-center justify-between mb-4">
32+
<div className="flex items-center gap-2">
33+
<Keyboard className="w-5 h-5 text-primary-400" />
34+
<h3 className="text-lg font-semibold text-white">快捷键</h3>
35+
</div>
36+
<button onClick={() => setOpen(false)} className="text-dark-400 hover:text-white p-1">
37+
<X className="w-5 h-5" />
38+
</button>
39+
</div>
40+
<div className="space-y-3">
41+
{SHORTCUTS.map(s => (
42+
<div key={s.keys} className="flex items-center justify-between">
43+
<span className="text-dark-300 text-sm">{s.desc}</span>
44+
<kbd className="px-2 py-1 bg-dark-700 text-dark-200 text-xs rounded border border-dark-600 font-mono">{s.keys}</kbd>
45+
</div>
46+
))}
47+
</div>
48+
<p className="text-dark-500 text-xs mt-4 text-center">按 Escape 或点击外部关闭</p>
49+
</div>
50+
</div>
51+
);
52+
}

frontend/src/pages/Submissions.tsx

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,38 @@ export default function Submissions() {
280280
</div>
281281
)}
282282

283+
{(submission.time_ms != null ||
284+
submission.memory_kb != null) && (
285+
<div className="mb-3 flex flex-wrap items-center gap-4">
286+
{submission.time_ms != null && (
287+
<div className="flex items-center gap-2">
288+
<span className="text-xs text-gray-500">耗时</span>
289+
<div className="w-32 h-2 bg-dark-700 rounded-full overflow-hidden">
290+
<div
291+
className="h-full bg-blue-500 rounded-full transition-all"
292+
style={{ width: `${Math.min((submission.time_ms / 2000) * 100, 100)}%` }}
293+
/>
294+
</div>
295+
<span className="text-xs text-gray-300 font-mono">{submission.time_ms}ms</span>
296+
</div>
297+
)}
298+
{submission.memory_kb != null && (
299+
<div className="flex items-center gap-2">
300+
<span className="text-xs text-gray-500">内存</span>
301+
<div className="w-32 h-2 bg-dark-700 rounded-full overflow-hidden">
302+
<div
303+
className="h-full bg-green-500 rounded-full transition-all"
304+
style={{ width: `${Math.min((submission.memory_kb / 65536) * 100, 100)}%` }}
305+
/>
306+
</div>
307+
<span className="text-xs text-gray-300 font-mono">
308+
{(submission.memory_kb / 1024).toFixed(1)}MB
309+
</span>
310+
</div>
311+
)}
312+
</div>
313+
)}
314+
283315
{submission.details?.cases &&
284316
submission.details.cases.length > 0 && (
285317
<div>
@@ -309,18 +341,56 @@ export default function Submissions() {
309341
{submission.details?.options &&
310342
submission.details?.user_answer !== undefined &&
311343
submission.details?.user_answer !== null && (
312-
<p className="text-gray-300 text-sm">
313-
选择的答案:第{' '}
314-
{Number(submission.details.user_answer) + 1}
315-
</p>
344+
<div className="mb-1">
345+
<p className="text-xs text-gray-500 mb-1">选择答案</p>
346+
<div className="flex flex-wrap gap-2">
347+
{submission.details.options.map(
348+
(opt, idx) => {
349+
const isUser = idx === Number(submission.details!.user_answer);
350+
const isCorrect =
351+
String(idx) === String(submission.details!.correct_answer);
352+
return (
353+
<span
354+
key={idx}
355+
className={`px-2 py-1 rounded text-xs border ${
356+
isUser && isCorrect
357+
? 'bg-green-600/20 text-green-400 border-green-600/40'
358+
: isUser
359+
? 'bg-red-600/20 text-red-400 border-red-600/40'
360+
: isCorrect
361+
? 'bg-green-600/10 text-green-500 border-green-600/20'
362+
: 'bg-dark-700 text-gray-400 border-dark-600'
363+
}`}
364+
>
365+
{String.fromCharCode(65 + idx)}. {opt}
366+
{isUser && ' ← 你的选择'}
367+
{isCorrect && ' ✓'}
368+
</span>
369+
);
370+
}
371+
)}
372+
</div>
373+
</div>
316374
)}
317375

318376
{submission.details?.acceptable_answers &&
319377
submission.details?.user_answer !== undefined &&
320378
submission.details?.user_answer !== null && (
321-
<p className="text-gray-300 text-sm">
322-
填写答案:{submission.details.user_answer}
323-
</p>
379+
<div className="mb-1">
380+
<p className="text-xs text-gray-500 mb-1">填空答案</p>
381+
<div className="flex items-center gap-2 flex-wrap">
382+
<span className={`px-2 py-1 rounded text-xs ${
383+
submission.details.acceptable_answers.includes(
384+
String(submission.details.user_answer)
385+
)
386+
? 'bg-green-600/20 text-green-400'
387+
: 'bg-red-600/20 text-red-400'
388+
}`}>
389+
你的答案:{submission.details.user_answer}
390+
</span>
391+
<span className="text-dark-500 text-xs">正确选项:{submission.details.acceptable_answers.join(' / ')}</span>
392+
</div>
393+
</div>
324394
)}
325395
</div>
326396
)}

frontend/tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"root":["./src/app.tsx","./src/main.tsx","./src/components/choicequestion.tsx","./src/components/codeeditor.tsx","./src/components/fillblankquestion.tsx","./src/components/footer.tsx","./src/components/gamificationsection.tsx","./src/components/markdownrenderer.tsx","./src/components/navbar.tsx","./src/components/problemcard.tsx","./src/components/submissionstatus.tsx","./src/context/authcontext.tsx","./src/context/bookmarkcontext.tsx","./src/hooks/usedocumenttitle.ts","./src/pages/admin.tsx","./src/pages/adminproblemform.tsx","./src/pages/home.tsx","./src/pages/leaderboard.tsx","./src/pages/login.tsx","./src/pages/notfound.tsx","./src/pages/problemdetail.tsx","./src/pages/problems.tsx","./src/pages/profile.tsx","./src/pages/register.tsx","./src/pages/submissions.tsx","./src/services/api.ts","./src/types/index.ts"],"version":"5.9.3"}
1+
{"root":["./src/app.tsx","./src/main.tsx","./src/components/choicequestion.tsx","./src/components/codeeditor.tsx","./src/components/errorboundary.tsx","./src/components/fillblankquestion.tsx","./src/components/footer.tsx","./src/components/gamificationsection.tsx","./src/components/keyboardshortcuts.tsx","./src/components/markdownrenderer.tsx","./src/components/navbar.tsx","./src/components/problemcard.tsx","./src/components/submissionstatus.tsx","./src/context/authcontext.tsx","./src/context/bookmarkcontext.tsx","./src/hooks/usedocumenttitle.ts","./src/pages/admin.tsx","./src/pages/adminproblemform.tsx","./src/pages/home.tsx","./src/pages/leaderboard.tsx","./src/pages/login.tsx","./src/pages/notfound.tsx","./src/pages/problemdetail.tsx","./src/pages/problems.tsx","./src/pages/profile.tsx","./src/pages/register.tsx","./src/pages/submissions.tsx","./src/services/api.ts","./src/types/index.ts"],"version":"5.9.3"}

0 commit comments

Comments
 (0)