|
|
@@ -0,0 +1,231 @@
|
|
|
+// challenge_list_screen.dart
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
+import 'package:intl/intl.dart';
|
|
|
+import 'package:japp_flutter/core/models/challenge_model.dart';
|
|
|
+import 'package:japp_flutter/features/challenge/view_models/challenge_list_vm.dart';
|
|
|
+
|
|
|
+class ChallengeListScreen extends ConsumerWidget {
|
|
|
+ const ChallengeListScreen({super.key});
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context, WidgetRef ref) {
|
|
|
+ final challengesState = ref.watch(challengeListProvider);
|
|
|
+
|
|
|
+ return Scaffold(
|
|
|
+ appBar: AppBar(
|
|
|
+ title: const Text('挑战列表'),
|
|
|
+ actions: [
|
|
|
+ IconButton(
|
|
|
+ icon: const Icon(Icons.refresh),
|
|
|
+ onPressed: () => ref.refresh(challengeListProvider.future),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ body: _buildBody(challengesState, ref),
|
|
|
+ floatingActionButton: FloatingActionButton(
|
|
|
+ onPressed: () => Navigator.pushNamed(context, '/challenge/add'),
|
|
|
+ child: const Icon(Icons.add),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _buildBody(AsyncValue<List<ChallengeModel>> state, WidgetRef ref) {
|
|
|
+ return state.when(
|
|
|
+ loading: () => const Center(child: CircularProgressIndicator()),
|
|
|
+ error: (error, stack) => Center(
|
|
|
+ child: Column(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
+ children: [
|
|
|
+ const Icon(Icons.error, color: Colors.red, size: 48),
|
|
|
+ const SizedBox(height: 16),
|
|
|
+ Text('加载失败: $error', style: const TextStyle(color: Colors.red)),
|
|
|
+ const SizedBox(height: 16),
|
|
|
+ ElevatedButton(
|
|
|
+ onPressed: () => ref.refresh(challengeListProvider.future),
|
|
|
+ child: const Text('重试'),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ data: (challenges) => challenges.isEmpty
|
|
|
+ ? const Center(child: Text('暂无挑战', style: TextStyle(fontSize: 18)))
|
|
|
+ : RefreshIndicator(
|
|
|
+ onRefresh: () => ref.read(challengeListProvider.notifier).refresh(),
|
|
|
+ child: ListView.builder(
|
|
|
+ padding: const EdgeInsets.all(16),
|
|
|
+ itemCount: challenges.length,
|
|
|
+ itemBuilder: (_, index) => ChallengeCard(challenge: challenges[index]),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class ChallengeCard extends ConsumerWidget {
|
|
|
+ final ChallengeModel challenge;
|
|
|
+
|
|
|
+ const ChallengeCard({super.key, required this.challenge});
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context, WidgetRef ref) {
|
|
|
+ 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),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ _DifficultyChip(difficulty: challenge.difficulty),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 8),
|
|
|
+ Text(
|
|
|
+ challenge.description,
|
|
|
+ style: TextStyle(color: Colors.grey[700], fontSize: 14),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 16),
|
|
|
+ _ProgressBar(
|
|
|
+ startDate: challenge.startDate,
|
|
|
+ endDate: challenge.endDate,
|
|
|
+ remainingDays: challenge.remainingDays!,
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _CardFooter(
|
|
|
+ participants: challenge.participants,
|
|
|
+ remainingDays: challenge.remainingDays!,
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 将复杂子组件拆分为独立组件
|
|
|
+class _DifficultyChip extends StatelessWidget {
|
|
|
+ final String difficulty;
|
|
|
+
|
|
|
+ const _DifficultyChip({required this.difficulty});
|
|
|
+
|
|
|
+ Color _getColor() {
|
|
|
+ switch (difficulty) {
|
|
|
+ case '简单': return Colors.green;
|
|
|
+ case '中等': return Colors.orange;
|
|
|
+ case '困难': return Colors.red;
|
|
|
+ default: return Colors.grey;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ return Chip(
|
|
|
+ label: Text(difficulty, style: const TextStyle(color: Colors.white)),
|
|
|
+ backgroundColor: _getColor(),
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class _ProgressBar extends StatelessWidget {
|
|
|
+ final DateTime startDate;
|
|
|
+ final DateTime endDate;
|
|
|
+ final int remainingDays;
|
|
|
+
|
|
|
+ const _ProgressBar({
|
|
|
+ required this.startDate,
|
|
|
+ required this.endDate,
|
|
|
+ required this.remainingDays,
|
|
|
+ });
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ final theme = Theme.of(context);
|
|
|
+ final totalDays = endDate.difference(startDate).inDays;
|
|
|
+ final passedDays = totalDays - 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(startDate), style: const TextStyle(fontSize: 12)),
|
|
|
+ Text(DateFormat('MM/dd').format(endDate), style: const TextStyle(fontSize: 12)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class _CardFooter extends StatelessWidget {
|
|
|
+ final int participants;
|
|
|
+ final int remainingDays;
|
|
|
+
|
|
|
+ const _CardFooter({
|
|
|
+ required this.participants,
|
|
|
+ required this.remainingDays,
|
|
|
+ });
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ return Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
+ children: [
|
|
|
+ _InfoChip(icon: Icons.people, text: '$participants人参加'),
|
|
|
+ _InfoChip(icon: Icons.calendar_today, text: '剩余${remainingDays}天'),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class _InfoChip extends StatelessWidget {
|
|
|
+ final IconData icon;
|
|
|
+ final String text;
|
|
|
+
|
|
|
+ const _InfoChip({
|
|
|
+ required this.icon,
|
|
|
+ required this.text,
|
|
|
+ });
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ 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])),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|