import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import '../services/auth_service.dart'; import 'package:timeago/timeago.dart' as timeago; class LeetCodeCard extends StatefulWidget { const LeetCodeCard({super.key}); @override State createState() => _LeetCodeCardState(); } class _LeetCodeCardState extends State with SingleTickerProviderStateMixin { late AnimationController _refreshController; List _submissions = []; Map? _progress; bool _isLoading = false; String? _error; Future _fetchData() async { setState(() { _isLoading = true; _error = null; }); try { final authService = Provider.of(context, listen: false); final cookie = authService.credentials['leetcode_cookie']; final userSlug = authService.credentials['leetcode_user_slug']; if (cookie == null || cookie.isEmpty) { throw Exception('未配置 LeetCode Cookie'); } if (userSlug == null || userSlug.isEmpty) { throw Exception('未配置 LeetCode User Slug'); } // 获取提交记录 final submissionsResponse = await http.post( Uri.parse('https://leetcode.cn/graphql/noj-go/'), headers: {'Content-Type': 'application/json', 'Cookie': cookie}, body: jsonEncode({ 'query': ''' query recentAcSubmissions(\$userSlug: String!) { recentACSubmissions(userSlug: \$userSlug) { submissionId submitTime question { title translatedTitle titleSlug questionFrontendId } } } ''', 'variables': {'userSlug': userSlug}, 'operationName': 'recentAcSubmissions', }), ); // 获取进度数据 final progressResponse = await http.post( Uri.parse('https://leetcode.cn/graphql/'), headers: {'Content-Type': 'application/json', 'Cookie': cookie}, body: jsonEncode({ 'query': ''' query userProfileUserQuestionProgressV2(\$userSlug: String!) { userProfileUserQuestionProgressV2(userSlug: \$userSlug) { numAcceptedQuestions { count difficulty } numFailedQuestions { count difficulty } numUntouchedQuestions { count difficulty } userSessionBeatsPercentage { difficulty percentage } totalQuestionBeatsPercentage } } ''', 'variables': {'userSlug': userSlug}, 'operationName': 'userProfileUserQuestionProgressV2', }), ); if (submissionsResponse.statusCode == 200 && progressResponse.statusCode == 200) { final submissionsData = jsonDecode(submissionsResponse.body); final progressData = jsonDecode(progressResponse.body); setState(() { _submissions = submissionsData['data']['recentACSubmissions']; _progress = progressData['data']['userProfileUserQuestionProgressV2']; _isLoading = false; }); } else { throw Exception('请求失败: ${submissionsResponse.statusCode}'); } } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } @override void initState() { super.initState(); _fetchData(); _refreshController = AnimationController( vsync: this, duration: const Duration(milliseconds: 500), ); } Widget _buildProgressCard() { if (_progress == null) return const SizedBox.shrink(); final acceptedQuestions = _progress!['numAcceptedQuestions'] as List; final totalBeatsPercentage = _progress!['totalQuestionBeatsPercentage'] as double; return Card( elevation: 3, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), margin: const EdgeInsets.symmetric(vertical: 8), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.bar_chart, color: Colors.green), SizedBox(width: 8), Text( '解题进度', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 12), ...acceptedQuestions.map((q) { final difficulty = q['difficulty'] as String; final count = q['count'] as int; Color difficultyColor = Colors.grey; if (difficulty == 'EASY') difficultyColor = Colors.green; else if (difficulty == 'MEDIUM') difficultyColor = Colors.orange; else if (difficulty == 'HARD') difficultyColor = Colors.red; return Padding( padding: const EdgeInsets.symmetric(vertical: 6.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: difficultyColor, shape: BoxShape.circle, ), ), const SizedBox(width: 8), Text( difficulty == 'EASY' ? '简单' : difficulty == 'MEDIUM' ? '中等' : '困难', style: const TextStyle(fontWeight: FontWeight.w500), ), ], ), Text('$count 题', style: const TextStyle(fontSize: 14)), ], ), ); }).toList(), const Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Row( children: [ Icon(Icons.workspace_premium, size: 18, color: Colors.green), SizedBox(width: 6), Text('击败用户', style: TextStyle(fontWeight: FontWeight.bold)), ], ), Text('${totalBeatsPercentage.toStringAsFixed(1)}%', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: Colors.green)), ], ), ], ), ), ); } @override Widget build(BuildContext context) { return Card( elevation: 4, shadowColor: Colors.deepPurple.withOpacity(0.3), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide(color: Colors.deepPurple.withOpacity(0.1)), ), margin: const EdgeInsets.symmetric(vertical: 8.0), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'LeetCode 最近提交', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.green, ), ), Container( decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(20), ), padding: const EdgeInsets.all(8), child: RotationTransition( turns: _refreshController, child: IconButton( icon: const Icon(Icons.refresh, size: 24, color: Colors.green), onPressed: _isLoading ? null : () async { _refreshController.forward(from: 0.0); await _fetchData(); }, ), ), ), ], ), if (_isLoading) Center( child: Padding( padding: const EdgeInsets.all(24.0), child: AnimatedBuilder( animation: _refreshController, builder: (_, child) => Transform.rotate( angle: _refreshController.value * 2 * 3.14, child: child, ), child: const CircularProgressIndicator( color: Colors.green, strokeWidth: 2, ), ), ), ) else if (_error != null) Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: Colors.red), const SizedBox(width: 8), Expanded( child: Text( _error!, style: const TextStyle(color: Colors.red), textAlign: TextAlign.center, ), ), ], ), ), ) else Expanded( child: SingleChildScrollView( child: Column( children: [ _buildProgressCard(), const SizedBox(height: 16), if (_submissions.isEmpty) const Center( child: Padding( padding: EdgeInsets.all(16.0), child: Text( '暂无提交记录', style: TextStyle(fontSize: 16, color: Colors.grey), ), ), ) else ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: _submissions.length, itemBuilder: (context, index) { final submission = _submissions[index]; final question = submission['question']; return Card( elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), margin: const EdgeInsets.symmetric(vertical: 4), child: ListTile( leading: CircleAvatar( backgroundColor: Colors.green, radius: 16, child: Text( question['questionFrontendId'].toString(), style: const TextStyle( color: Colors.white, fontSize: 12, ), ), ), title: Text( question['translatedTitle'] ?? question['title'], style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), subtitle: Text( '提交时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(submission['submitTime'] * 1000), locale: 'zh_CN')}', style: const TextStyle(fontSize: 14), ), trailing: const Icon( Icons.check_circle, color: Colors.green, ), ), ); }, ), ], ), ), ), ], ), ), ); } }