Skip to content

Commit 11c594a

Browse files
author
CodeJudge
committed
feat: 提交标题可点击 + 下载代码 + 管理员查看所有提交
1 parent 7ac0771 commit 11c594a

3 files changed

Lines changed: 108 additions & 15 deletions

File tree

backend/src/routes/submissions.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
const router = require('express').Router();
22
const { submit, listSubmissions, getSubmission } = require('../controllers/submissionController');
3-
const { authenticate } = require('../middleware/auth');
3+
const { authenticate, adminOnly } = require('../middleware/auth');
4+
const { queryAll } = require('../config/db');
45

56
router.post('/', authenticate, submit);
67
router.get('/', authenticate, listSubmissions);
8+
router.get('/admin/all', authenticate, adminOnly, (req, res) => {
9+
const { page = 1, limit = 20 } = req.query;
10+
const offset = (Number(page) - 1) * Number(limit);
11+
const total = queryAll('SELECT COUNT(*) as count FROM submissions')[0].count;
12+
const submissions = queryAll(
13+
`SELECT s.id, s.user_id, s.problem_id, s.status, s.score, s.language, s.created_at,
14+
u.username, p.title as problem_title
15+
FROM submissions s
16+
LEFT JOIN users u ON u.id = s.user_id
17+
LEFT JOIN problems p ON p.id = s.problem_id
18+
ORDER BY s.created_at DESC LIMIT ? OFFSET ?`,
19+
[Number(limit), offset]
20+
);
21+
res.json({ submissions, total, page: Number(page), totalPages: Math.ceil(total / Number(limit)) });
22+
});
723
router.get('/:id', authenticate, getSubmission);
824

925
module.exports = router;

frontend/src/pages/Admin.tsx

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useState, useEffect, useCallback } from 'react';
2-
import { useNavigate } from 'react-router-dom';
2+
import { useNavigate, Link } from 'react-router-dom';
33
import { Plus, Edit, Trash2, Users, Shield, Code, BarChart3, AlertTriangle, Loader2, Activity, Percent, Clock, Download, Upload } from 'lucide-react';
44
import toast from 'react-hot-toast';
55
import { useAuth } from '../context/AuthContext';
66
import api from '../services/api';
77
import type { Problem, ProblemStats, AdminStats } from '../types';
8+
import SubmissionStatus from '../components/SubmissionStatus';
89

910
export default function Admin() {
1011
const { user } = useAuth();
@@ -18,6 +19,8 @@ export default function Admin() {
1819
const [deletingId, setDeletingId] = useState<number | null>(null);
1920
const [users, setUsers] = useState<any[]>([]);
2021
const [usersLoading, setUsersLoading] = useState(false);
22+
const [allSubmissions, setAllSubmissions] = useState<any[]>([]);
23+
const [allLoading, setAllLoading] = useState(false);
2124

2225
const fetchData = useCallback(async () => {
2326
setLoading(true);
@@ -116,6 +119,17 @@ export default function Admin() {
116119
} catch { toast.error('删除失败'); }
117120
};
118121

122+
const fetchAllSubmissions = async () => {
123+
setAllLoading(true);
124+
try {
125+
const token = localStorage.getItem('oj_token');
126+
const res = await fetch('/api/submissions/admin/all', { headers: { Authorization: `Bearer ${token}` } });
127+
const data = await res.json();
128+
setAllSubmissions(data.submissions || []);
129+
} catch { toast.error('加载失败'); }
130+
finally { setAllLoading(false); }
131+
};
132+
119133
const TYPE_LABELS: Record<string, string> = {
120134
programming: '编程题',
121135
choice: '选择题',
@@ -385,8 +399,52 @@ export default function Admin() {
385399
))}
386400
</tbody>
387401
</table>
388-
</div>
389-
)}
402+
</div>
403+
)}
404+
405+
{/* All Submissions */}
406+
<div className="card p-6 mt-6">
407+
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
408+
<Activity className="w-5 h-5 text-cyan-400" /> 所有提交
409+
</h2>
410+
{allSubmissions.length === 0 ? (
411+
<button onClick={fetchAllSubmissions} className="btn-secondary text-sm inline-flex items-center gap-2">
412+
{allLoading && <Loader2 className="w-4 h-4 animate-spin" />}
413+
加载提交记录
414+
</button>
415+
) : (
416+
<div className="overflow-x-auto max-h-96 overflow-y-auto">
417+
<table className="w-full text-sm">
418+
<thead>
419+
<tr className="border-b border-dark-700">
420+
<th className="text-left py-2 px-3 text-dark-400">ID</th>
421+
<th className="text-left py-2 px-3 text-dark-400">用户</th>
422+
<th className="text-left py-2 px-3 text-dark-400">题目</th>
423+
<th className="text-left py-2 px-3 text-dark-400">状态</th>
424+
<th className="text-left py-2 px-3 text-dark-400 hidden md:table-cell">时间</th>
425+
</tr>
426+
</thead>
427+
<tbody>
428+
{allSubmissions.map((s: any) => (
429+
<tr key={s.id} className="border-b border-dark-800 hover:bg-dark-800/50">
430+
<td className="py-2 px-3 text-dark-400">{s.id}</td>
431+
<td className="py-2 px-3 text-white">{s.username}</td>
432+
<td className="py-2 px-3">
433+
<Link to={`/problems/${s.problem_id}`} className="text-blue-400 hover:text-blue-300">
434+
{s.problem_title || `#${s.problem_id}`}
435+
</Link>
436+
</td>
437+
<td className="py-2 px-3">
438+
<SubmissionStatus status={s.status} score={s.score} />
439+
</td>
440+
<td className="py-2 px-3 text-dark-400 hidden md:table-cell text-xs">{s.created_at?.slice(0, 16).replace('T', ' ')}</td>
441+
</tr>
442+
))}
443+
</tbody>
444+
</table>
445+
</div>
446+
)}
447+
</div>
390448
</div>
391449
);
392450
}

frontend/src/pages/Submissions.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,13 @@ export default function Submissions() {
239239
>
240240
<div className="grid grid-cols-6 items-center py-2 hover:bg-dark-800/50 rounded transition-colors">
241241
<div className="px-4 text-white font-medium truncate">
242-
{submission.problem_title ??
243-
`#${submission.problem_id}`}
242+
<Link
243+
to={`/problems/${submission.problem_id}`}
244+
onClick={(e) => e.stopPropagation()}
245+
className="hover:text-primary-400 transition-colors"
246+
>
247+
{submission.problem_title ?? `#${submission.problem_id}`}
248+
</Link>
244249
</div>
245250
<div className="px-4">
246251
<SubmissionStatus
@@ -277,15 +282,29 @@ export default function Submissions() {
277282
</Link>
278283
{submission.code && (
279284
<div className="mb-3">
280-
<div className="flex items-center justify-between mb-1">
281-
<p className="text-xs text-dark-500">提交代码</p>
282-
<button
283-
onClick={() => { navigator.clipboard.writeText(submission.code); toast.success('代码已复制'); }}
284-
className="flex items-center gap-1 text-xs text-dark-400 hover:text-white transition-colors"
285-
>
286-
<Clipboard className="w-3.5 h-3.5" /> 复制
287-
</button>
288-
</div>
285+
<div className="flex items-center justify-between mb-1">
286+
<p className="text-xs text-dark-500">提交代码</p>
287+
<div className="flex items-center gap-2">
288+
<button
289+
onClick={() => {
290+
const blob = new Blob([submission.code], { type: 'text/plain' });
291+
const url = URL.createObjectURL(blob);
292+
const a = document.createElement('a'); a.href = url;
293+
a.download = `submission_${submission.id}.${submission.language === 'python' ? 'py' : 'js'}`;
294+
a.click(); URL.revokeObjectURL(url);
295+
}}
296+
className="flex items-center gap-1 text-xs text-dark-400 hover:text-white transition-colors"
297+
>
298+
<Send className="w-3.5 h-3.5" /> 下载
299+
</button>
300+
<button
301+
onClick={() => { navigator.clipboard.writeText(submission.code); toast.success('代码已复制'); }}
302+
className="flex items-center gap-1 text-xs text-dark-400 hover:text-white transition-colors"
303+
>
304+
<Clipboard className="w-3.5 h-3.5" /> 复制
305+
</button>
306+
</div>
307+
</div>
289308
<pre className="bg-dark-900 rounded p-3 text-sm text-dark-300 overflow-auto max-h-60">{submission.code}</pre>
290309
</div>
291310
)}

0 commit comments

Comments
 (0)