Skip to content

Commit 3de51ca

Browse files
author
CodeJudge
committed
feat: 语言使用统计图表
1 parent 6d8e3e2 commit 3de51ca

2 files changed

Lines changed: 42 additions & 1 deletion

File tree

backend/src/routes/auth.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ router.get('/streak', authenticate, (req, res) => {
3636
res.json({ streak, todayChecked });
3737
});
3838

39+
router.get('/language-stats', authenticate, (req, res) => {
40+
const stats = queryAll(
41+
'SELECT language, COUNT(*) as count FROM submissions WHERE user_id = ? AND language IS NOT NULL AND language != "" GROUP BY language ORDER BY count DESC',
42+
[req.user.id]
43+
);
44+
res.json({ languages: stats });
45+
});
46+
3947
router.get('/leaderboard', async (req, res) => {
4048
try {
4149
const rows = queryAll(`

frontend/src/pages/Profile.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect } from 'react';
22
import { Link } from 'react-router-dom';
3-
import { User, Mail, Shield, Award, TrendingUp, Loader2, AlertTriangle, CheckCircle2, XCircle, Clock, Zap, Lock, Key, CalendarDays } from 'lucide-react';
3+
import { User, Mail, Shield, Award, TrendingUp, Loader2, AlertTriangle, CheckCircle2, XCircle, Clock, Zap, Lock, Key, CalendarDays, Code } from 'lucide-react';
44
import { useAuth } from '../context/AuthContext';
55
import api from '../services/api';
66
import type { Submission } from '../types';
@@ -50,6 +50,7 @@ export default function Profile() {
5050
const [passwordMessage, setPasswordMessage] = useState<string | null>(null);
5151
const [passwordError, setPasswordError] = useState<string | null>(null);
5252
const [calendarDates, setCalendarDates] = useState<Set<string>>(new Set());
53+
const [languageStats, setLanguageStats] = useState<{language: string; count: number}[]>([]);
5354

5455
useEffect(() => {
5556
const token = localStorage.getItem('oj_token');
@@ -60,6 +61,12 @@ export default function Profile() {
6061
setCalendarDates(new Set(data.dates || []));
6162
})
6263
.catch(() => {});
64+
fetch('/api/auth/language-stats', { headers: { Authorization: `Bearer ${token}` } })
65+
.then(r => r.json())
66+
.then(data => {
67+
setLanguageStats(data.languages || []);
68+
})
69+
.catch(() => {});
6370
}, []);
6471

6572
useEffect(() => {
@@ -206,6 +213,32 @@ export default function Profile() {
206213
</div>
207214
</div>
208215

216+
{/* Language usage */}
217+
{languageStats.length > 0 && (
218+
<div className="card p-6">
219+
<h3 className="text-sm font-semibold text-dark-200 mb-3 flex items-center gap-2">
220+
<Code className="w-4 h-4 text-blue-400" /> 使用语言统计
221+
</h3>
222+
<div className="space-y-2">
223+
{languageStats.map((lang: {language: string; count: number}) => {
224+
const maxCount = Math.max(...languageStats.map((l: any) => l.count));
225+
return (
226+
<div key={lang.language} className="flex items-center gap-3">
227+
<span className="w-24 text-xs text-dark-400 text-right">{lang.language || '未知'}</span>
228+
<div className="flex-1 h-4 bg-dark-700 rounded-full overflow-hidden">
229+
<div
230+
className="h-full bg-primary-500 rounded-full transition-all duration-500"
231+
style={{ width: `${(lang.count / maxCount) * 100}%` }}
232+
/>
233+
</div>
234+
<span className="text-xs text-dark-400 w-8">{lang.count}</span>
235+
</div>
236+
);
237+
})}
238+
</div>
239+
</div>
240+
)}
241+
209242
<GamificationSection />
210243

211244
{/* Solved calendar */}

0 commit comments

Comments
 (0)