|
@@ -0,0 +1,216 @@
|
|
|
|
|
+import 'package:flutter/material.dart';
|
|
|
|
|
+import 'package:intl/intl.dart';
|
|
|
|
|
+import 'package:japp_flutter/core/models/challenge_model.dart';
|
|
|
|
|
+import 'package:japp_flutter/features/challenge/view_models/challenge_view_model.dart';
|
|
|
|
|
+import 'package:provider/provider.dart';
|
|
|
|
|
+
|
|
|
|
|
+class ChallengeListScreen extends StatefulWidget {
|
|
|
|
|
+ const ChallengeListScreen({super.key});
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ State<ChallengeListScreen> createState() => _ChallengeListScreenState();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class _ChallengeListScreenState extends State<ChallengeListScreen> {
|
|
|
|
|
+ @override
|
|
|
|
|
+ void initState() {
|
|
|
|
|
+ super.initState();
|
|
|
|
|
+ WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
+ Provider.of<ChallengeListViewModel>(context, listen: false).fetchChallenges();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ Widget build(BuildContext context) {
|
|
|
|
|
+ final viewModel = Provider.of<ChallengeListViewModel>(context);
|
|
|
|
|
+
|
|
|
|
|
+ return Scaffold(
|
|
|
|
|
+ appBar: AppBar(
|
|
|
|
|
+ title: const Text('挑战列表'),
|
|
|
|
|
+ actions: [
|
|
|
|
|
+ IconButton(
|
|
|
|
|
+ icon: const Icon(Icons.refresh),
|
|
|
|
|
+ onPressed: viewModel.fetchChallenges,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ body: _buildBody(viewModel),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Widget _buildBody(ChallengeListViewModel viewModel) {
|
|
|
|
|
+ if (viewModel.isLoading) {
|
|
|
|
|
+ return const Center(child: CircularProgressIndicator());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (viewModel.error.isNotEmpty) {
|
|
|
|
|
+ return Center(
|
|
|
|
|
+ child: Column(
|
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ const Icon(Icons.error, color: Colors.red, size: 48),
|
|
|
|
|
+ const SizedBox(height: 16),
|
|
|
|
|
+ Text(viewModel.error, style: const TextStyle(color: Colors.red)),
|
|
|
|
|
+ const SizedBox(height: 16),
|
|
|
|
|
+ ElevatedButton(
|
|
|
|
|
+ onPressed: viewModel.fetchChallenges,
|
|
|
|
|
+ child: const Text('重试'),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (viewModel.challenges.isEmpty) {
|
|
|
|
|
+ return const Center(
|
|
|
|
|
+ child: Text('暂无挑战', style: TextStyle(fontSize: 18)),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return RefreshIndicator(
|
|
|
|
|
+ onRefresh: viewModel.fetchChallenges,
|
|
|
|
|
+ child: ListView.builder(
|
|
|
|
|
+ padding: const EdgeInsets.all(16),
|
|
|
|
|
+ itemCount: viewModel.challenges.length,
|
|
|
|
|
+ itemBuilder: (context, index) {
|
|
|
|
|
+ return ChallengeCard(challenge: viewModel.challenges[index]);
|
|
|
|
|
+ },
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class ChallengeCard extends StatelessWidget {
|
|
|
|
|
+ final ChallengeModel challenge;
|
|
|
|
|
+
|
|
|
|
|
+ const ChallengeCard({super.key, required this.challenge});
|
|
|
|
|
+
|
|
|
|
|
+ Color _getDifficultyColor() {
|
|
|
|
|
+ switch (challenge.difficulty) {
|
|
|
|
|
+ case '简单':
|
|
|
|
|
+ return Colors.green;
|
|
|
|
|
+ case '中等':
|
|
|
|
|
+ return Colors.orange;
|
|
|
|
|
+ case '困难':
|
|
|
|
|
+ return Colors.red;
|
|
|
|
|
+ default:
|
|
|
|
|
+ return Colors.grey;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ Widget build(BuildContext context) {
|
|
|
|
|
+ final theme = Theme.of(context); // 获取当前主题
|
|
|
|
|
+
|
|
|
|
|
+ return Card(
|
|
|
|
|
+ elevation: 2,
|
|
|
|
|
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
|
|
|
+ margin: const EdgeInsets.only(bottom: 16),
|
|
|
|
|
+ child: Padding(
|
|
|
|
|
+ padding: const EdgeInsets.all(16),
|
|
|
|
|
+ child: Column(
|
|
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Row(
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Expanded(
|
|
|
|
|
+ child: Text(
|
|
|
|
|
+ challenge.title,
|
|
|
|
|
+ style: const TextStyle(
|
|
|
|
|
+ fontSize: 18,
|
|
|
|
|
+ fontWeight: FontWeight.bold,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ Chip(
|
|
|
|
|
+ label: Text(
|
|
|
|
|
+ challenge.difficulty,
|
|
|
|
|
+ style: const TextStyle(color: Colors.white),
|
|
|
|
|
+ ),
|
|
|
|
|
+ backgroundColor: _getDifficultyColor(),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 8),
|
|
|
|
|
+ Text(
|
|
|
|
|
+ challenge.description,
|
|
|
|
|
+ style: TextStyle(color: Colors.grey[700], fontSize: 14),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 16),
|
|
|
|
|
+ _buildProgressBar(context),
|
|
|
|
|
+ const SizedBox(height: 12),
|
|
|
|
|
+ Row(
|
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ _buildInfoChip(
|
|
|
|
|
+ Icons.people,
|
|
|
|
|
+ '${challenge.participants}人参加',
|
|
|
|
|
+ ),
|
|
|
|
|
+ _buildInfoChip(
|
|
|
|
|
+ Icons.calendar_today,
|
|
|
|
|
+ '剩余${challenge.remainingDays}天',
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Widget _buildProgressBar(BuildContext context) {
|
|
|
|
|
+ final theme = Theme.of(context);
|
|
|
|
|
+ final totalDays = challenge.endDate.difference(challenge.startDate).inDays;
|
|
|
|
|
+ final passedDays = totalDays - challenge.remainingDays;
|
|
|
|
|
+ final progress = passedDays / totalDays;
|
|
|
|
|
+
|
|
|
|
|
+ return Column(
|
|
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Text(
|
|
|
|
|
+ '${(progress * 100).toStringAsFixed(0)}%',
|
|
|
|
|
+ style: const TextStyle(fontSize: 12),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 4),
|
|
|
|
|
+ LinearProgressIndicator(
|
|
|
|
|
+ value: progress,
|
|
|
|
|
+ backgroundColor: Colors.grey[200],
|
|
|
|
|
+ borderRadius: BorderRadius.circular(10),
|
|
|
|
|
+ minHeight: 8,
|
|
|
|
|
+ color: theme.primaryColor, // 使用主题色
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 4),
|
|
|
|
|
+ Row(
|
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Text(
|
|
|
|
|
+ DateFormat('MM/dd').format(challenge.startDate),
|
|
|
|
|
+ style: const TextStyle(fontSize: 12),
|
|
|
|
|
+ ),
|
|
|
|
|
+ Text(
|
|
|
|
|
+ DateFormat('MM/dd').format(challenge.endDate),
|
|
|
|
|
+ style: const TextStyle(fontSize: 12),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Widget _buildInfoChip(IconData icon, String text) {
|
|
|
|
|
+ return Container(
|
|
|
|
|
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
|
|
|
+ decoration: BoxDecoration(
|
|
|
|
|
+ color: Colors.grey[100],
|
|
|
|
|
+ borderRadius: BorderRadius.circular(20),
|
|
|
|
|
+ ),
|
|
|
|
|
+ child: Row(
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Icon(icon, size: 16, color: Colors.grey[600]),
|
|
|
|
|
+ const SizedBox(width: 4),
|
|
|
|
|
+ Text(text, style: TextStyle(color: Colors.grey[700])),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|