ValiZhang 11 mesi fa
parent
commit
736ddfa257

+ 0 - 0
lib/app/app.dart


+ 0 - 0
lib/app/routes/app_router.dart


+ 0 - 0
lib/app/theme/app_theme.dart


+ 26 - 0
lib/core/models/challenge_model.dart

@@ -0,0 +1,26 @@
+class ChallengeModel {
+  final String id;
+  final String title;
+  final String description;
+  final DateTime startDate;
+  final DateTime endDate;
+  final int participants;
+  final bool completed;
+  final String difficulty;
+
+  ChallengeModel({
+    required this.id,
+    required this.title,
+    required this.description,
+    required this.startDate,
+    required this.endDate,
+    required this.participants,
+    required this.completed,
+    required this.difficulty,
+  });
+
+  // 获取剩余天数
+  int get remainingDays {
+    return endDate.difference(DateTime.now()).inDays;
+  }
+}

+ 0 - 0
lib/features/challenge/repositories/challenge_repository.dart


+ 74 - 0
lib/features/challenge/view_models/challenge_view_model.dart

@@ -0,0 +1,74 @@
+import 'package:flutter/material.dart';
+import 'package:japp_flutter/core/models/challenge_model.dart';
+
+class ChallengeListViewModel extends ChangeNotifier {
+  List<ChallengeModel> _challenges = [];
+  bool _isLoading = false;
+  String _error = '';
+
+  List<ChallengeModel> get challenges => _challenges;
+  bool get isLoading => _isLoading;
+  String get error => _error;
+
+  // 模拟数据加载
+  Future<void> fetchChallenges() async {
+    try {
+      _isLoading = true;
+      notifyListeners();
+      
+      // 模拟网络请求延迟
+      await Future.delayed(const Duration(seconds: 1));
+      
+      // 生成模拟数据
+      _challenges = [
+        ChallengeModel(
+          id: '1',
+          title: '30天健身挑战',
+          description: '每天坚持30分钟有氧运动,塑造健康体魄',
+          startDate: DateTime.now().subtract(const Duration(days: 5)),
+          endDate: DateTime.now().add(const Duration(days: 25)),
+          participants: 2541,
+          completed: false,
+          difficulty: '中等',
+        ),
+        ChallengeModel(
+          id: '2',
+          title: '每日阅读打卡',
+          description: '连续21天每天阅读至少30分钟',
+          startDate: DateTime.now().subtract(const Duration(days: 10)),
+          endDate: DateTime.now().add(const Duration(days: 11)),
+          participants: 4217,
+          completed: false,
+          difficulty: '简单',
+        ),
+        ChallengeModel(
+          id: '3',
+          title: '编程马拉松',
+          description: '7天内完成一个完整的Flutter应用',
+          startDate: DateTime.now().subtract(const Duration(days: 2)),
+          endDate: DateTime.now().add(const Duration(days: 5)),
+          participants: 876,
+          completed: false,
+          difficulty: '困难',
+        ),
+        ChallengeModel(
+          id: '4',
+          title: '素食30天挑战',
+          description: '连续30天坚持素食饮食',
+          startDate: DateTime.now().add(const Duration(days: 2)),
+          endDate: DateTime.now().add(const Duration(days: 32)),
+          participants: 1589,
+          completed: false,
+          difficulty: '中等',
+        ),
+      ];
+      
+      _error = '';
+    } catch (e) {
+      _error = '加载数据失败: $e';
+    } finally {
+      _isLoading = false;
+      notifyListeners();
+    }
+  }
+}

+ 216 - 0
lib/features/challenge/views/challenge_view.dart

@@ -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])),
+        ],
+      ),
+    );
+  }
+}

+ 16 - 110
lib/main.dart

@@ -1,122 +1,28 @@
 import 'package:flutter/material.dart';
+import 'package:flutter/foundation.dart';
+import 'package:intl/intl.dart';
+import 'package:japp_flutter/features/challenge/view_models/challenge_view_model.dart';
+import 'package:japp_flutter/features/challenge/views/challenge_view.dart';
+import 'package:provider/provider.dart';
 
-void main() {
-  runApp(const MyApp());
-}
+void main() => runApp(const ChallengeApp());
 
-class MyApp extends StatelessWidget {
-  const MyApp({super.key});
+class ChallengeApp extends StatelessWidget {
+  const ChallengeApp({super.key});
 
-  // This widget is the root of your application.
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
-      title: 'Flutter Demo',
+      title: '挑战列表',
+      debugShowCheckedModeBanner: false,
       theme: ThemeData(
-        // This is the theme of your application.
-        //
-        // TRY THIS: Try running your application with "flutter run". You'll see
-        // the application has a purple toolbar. Then, without quitting the app,
-        // try changing the seedColor in the colorScheme below to Colors.green
-        // and then invoke "hot reload" (save your changes or press the "hot
-        // reload" button in a Flutter-supported IDE, or press "r" if you used
-        // the command line to start the app).
-        //
-        // Notice that the counter didn't reset back to zero; the application
-        // state is not lost during the reload. To reset the state, use hot
-        // restart instead.
-        //
-        // This works for code too, not just values: Most code changes can be
-        // tested with just a hot reload.
-        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
+        primarySwatch: Colors.blue,
+        appBarTheme: const AppBarTheme(elevation: 1),
       ),
-      home: const MyHomePage(title: 'Flutter Demo Home Page'),
-    );
-  }
-}
-
-class MyHomePage extends StatefulWidget {
-  const MyHomePage({super.key, required this.title});
-
-  // This widget is the home page of your application. It is stateful, meaning
-  // that it has a State object (defined below) that contains fields that affect
-  // how it looks.
-
-  // This class is the configuration for the state. It holds the values (in this
-  // case the title) provided by the parent (in this case the App widget) and
-  // used by the build method of the State. Fields in a Widget subclass are
-  // always marked "final".
-
-  final String title;
-
-  @override
-  State<MyHomePage> createState() => _MyHomePageState();
-}
-
-class _MyHomePageState extends State<MyHomePage> {
-  int _counter = 0;
-
-  void _incrementCounter() {
-    setState(() {
-      // This call to setState tells the Flutter framework that something has
-      // changed in this State, which causes it to rerun the build method below
-      // so that the display can reflect the updated values. If we changed
-      // _counter without calling setState(), then the build method would not be
-      // called again, and so nothing would appear to happen.
-      _counter++;
-    });
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    // This method is rerun every time setState is called, for instance as done
-    // by the _incrementCounter method above.
-    //
-    // The Flutter framework has been optimized to make rerunning build methods
-    // fast, so that you can just rebuild anything that needs updating rather
-    // than having to individually change instances of widgets.
-    return Scaffold(
-      appBar: AppBar(
-        // TRY THIS: Try changing the color here to a specific color (to
-        // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
-        // change color while the other colors stay the same.
-        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
-        // Here we take the value from the MyHomePage object that was created by
-        // the App.build method, and use it to set our appbar title.
-        title: Text(widget.title),
-      ),
-      body: Center(
-        // Center is a layout widget. It takes a single child and positions it
-        // in the middle of the parent.
-        child: Column(
-          // Column is also a layout widget. It takes a list of children and
-          // arranges them vertically. By default, it sizes itself to fit its
-          // children horizontally, and tries to be as tall as its parent.
-          //
-          // Column has various properties to control how it sizes itself and
-          // how it positions its children. Here we use mainAxisAlignment to
-          // center the children vertically; the main axis here is the vertical
-          // axis because Columns are vertical (the cross axis would be
-          // horizontal).
-          //
-          // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
-          // action in the IDE, or press "p" in the console), to see the
-          // wireframe for each widget.
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: <Widget>[
-            const Text('You have pushed the button this many times:'),
-            Text(
-              '$_counter',
-              style: Theme.of(context).textTheme.headlineMedium,
-            ),
-          ],
-        ),
+      home: ChangeNotifierProvider(
+        create: (_) => ChallengeListViewModel(),
+        child: const ChallengeListScreen(),
       ),
-      floatingActionButton: FloatingActionButton(
-        onPressed: _incrementCounter,
-        tooltip: 'Increment',
-        child: const Icon(Icons.add),
-      ), // This trailing comma makes auto-formatting nicer for build methods.
     );
   }
-}
+}

+ 64 - 0
pubspec.lock

@@ -49,6 +49,22 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.8"
+  dio:
+    dependency: "direct main"
+    description:
+      name: dio
+      sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "5.8.0+1"
+  dio_web_adapter:
+    dependency: transitive
+    description:
+      name: dio_web_adapter
+      sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.1"
   fake_async:
     dependency: transitive
     description:
@@ -75,6 +91,22 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.1.2"
+  intl:
+    dependency: "direct main"
+    description:
+      name: intl
+      sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.20.2"
   leak_tracker:
     dependency: transitive
     description:
@@ -131,6 +163,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.16.0"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0"
   path:
     dependency: transitive
     description:
@@ -139,6 +179,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.9.1"
+  provider:
+    dependency: "direct main"
+    description:
+      name: provider
+      sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.1.5"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -192,6 +240,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.7.4"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.4.0"
   vector_math:
     dependency: transitive
     description:
@@ -208,6 +264,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "15.0.0"
+  web:
+    dependency: transitive
+    description:
+      name: web
+      sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.1"
 sdks:
   dart: ">=3.8.1 <4.0.0"
   flutter: ">=3.18.0-18.0.pre.54"

+ 4 - 1
pubspec.yaml

@@ -34,7 +34,10 @@ dependencies:
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.8
-
+  intl: ^0.20.2
+  provider: ^6.0.0 # 核心状态管理
+  dio: ^5.0.0       # 网络请求
+  
 dev_dependencies:
   flutter_test:
     sdk: flutter