Ver código fonte

仍存在问题的一波提交

ValiZhang 11 meses atrás
pai
commit
bd265d2450

+ 2 - 1
android/app/build.gradle.kts

@@ -8,7 +8,8 @@ plugins {
 android {
     namespace = "com.example.japp_flutter"
     compileSdk = flutter.compileSdkVersion
-    ndkVersion = flutter.ndkVersion
+    // ndkVersion = flutter.ndkVersion
+    ndkVersion = project.properties["org.gradle.ndk.version"] as String
 
     compileOptions {
         sourceCompatibility = JavaVersion.VERSION_11

+ 1 - 0
android/gradle.properties

@@ -3,3 +3,4 @@ systemProp.https.proxyHost=mirrors.aliyun.com
 org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
 android.useAndroidX=true
 android.enableJetifier=true
+org.gradle.ndk.version=28.0.12674087

+ 3 - 0
devtools_options.yaml

@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:

+ 55 - 6
lib/core/models/challenge_model.dart

@@ -1,5 +1,8 @@
+import 'package:flutter/foundation.dart';
+
+@immutable // 标记为不可变类
 class ChallengeModel {
-  final String id;
+  final int? id;
   final String title;
   final String description;
   final DateTime startDate;
@@ -7,8 +10,9 @@ class ChallengeModel {
   final int participants;
   final bool completed;
   final String difficulty;
+  final int? remainingDays; // 改为可选字段
 
-  ChallengeModel({
+  const ChallengeModel({
     required this.id,
     required this.title,
     required this.description,
@@ -17,10 +21,55 @@ class ChallengeModel {
     required this.participants,
     required this.completed,
     required this.difficulty,
+    this.remainingDays, // 可选计算字段
   });
 
-  // 获取剩余天数
-  int get remainingDays {
-    return endDate.difference(DateTime.now()).inDays;
+  int get calculatedRemainingDays => endDate.difference(DateTime.now()).inDays;
+
+  factory ChallengeModel.fromJson(Map<String, dynamic> json) {
+    return ChallengeModel(
+      id: json['id'] as int?,
+      title: json['title'] as String,
+      description: json['description'] as String,
+      startDate: DateTime.parse(json['startDate'] as String),
+      endDate: DateTime.parse(json['endDate'] as String),
+      participants: json['participants'] as int,
+      completed: json['completed'] as bool,
+      difficulty: json['difficulty'] as String,
+    );
+  }
+
+  /// 转换为JSON
+  Map<String, dynamic> toJson() => {
+    if (id != null) 'id': id,
+    'title': title,
+    'description': description,
+    'startDate': startDate.toIso8601String(),
+    'endDate': endDate.toIso8601String(),
+    'participants': participants,
+    'completed': completed,
+    'difficulty': difficulty,
+  };
+
+  ChallengeModel copyWith({
+    int? id,
+    String? title,
+    String? description,
+    DateTime? startDate,
+    DateTime? endDate,
+    int? participants,
+    bool? completed,
+    String? difficulty,
+  }) {
+    return ChallengeModel(
+      id: id ?? this.id,
+      title: title ?? this.title,
+      description: description ?? this.description,
+      startDate: startDate ?? this.startDate,
+      endDate: endDate ?? this.endDate,
+      participants: participants ?? this.participants,
+      completed: completed ?? this.completed,
+      difficulty: difficulty ?? this.difficulty,
+    );
   }
-}
+}

+ 6 - 0
lib/core/proviers/database_providers.dart

@@ -0,0 +1,6 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:japp_flutter/data/local/database_helper.dart';
+
+final databaseHelperProvider = Provider<DatabaseHelper>((ref) {
+  return DatabaseHelper(); // 单例模式
+});

+ 10 - 0
lib/core/proviers/repository_providers.dart

@@ -0,0 +1,10 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:japp_flutter/core/proviers/database_providers.dart';
+import 'package:japp_flutter/core/repositories/challenge_repository.dart';
+import 'package:japp_flutter/core/repositories/sqlite_challenge_repository.dart';
+
+// 基础 Repository 提供者(全局可用)
+final challengeRepositoryProvider = Provider<ChallengeRepository>((ref) {
+  // 依赖注入 DatabaseHelper
+  return SqliteChallengeRepository(ref.read(databaseHelperProvider));
+});

+ 0 - 0
lib/features/challenge/repositories/challenge_repository.dart → lib/core/repositories/card_repository.dart


+ 9 - 0
lib/core/repositories/challenge_repository.dart

@@ -0,0 +1,9 @@
+import 'package:japp_flutter/core/models/challenge_model.dart';
+
+abstract class ChallengeRepository {
+  Future<List<ChallengeModel>> getAllChallenges();
+  Future<ChallengeModel?> getChallengeById(int id);
+  Future<ChallengeModel> addChallenge(ChallengeModel challenge);
+  Future<ChallengeModel> updateChallenge(ChallengeModel challenge);
+  Future<void> deleteChallenge(int id);
+}

+ 52 - 0
lib/core/repositories/mock_challenge_repository.dart

@@ -0,0 +1,52 @@
+import 'package:japp_flutter/core/models/challenge_model.dart';
+import 'package:japp_flutter/core/repositories/challenge_repository.dart';
+
+
+class MockChallengeRepository implements ChallengeRepository {
+  final List<ChallengeModel> _mockData = [
+    ChallengeModel(
+      id: 1,
+      title: 'Learn Flutter',
+      startDate: DateTime.now(),
+      endDate: DateTime.now().add(Duration(days: 30)), 
+      description: '', 
+      participants: 1, 
+      completed: false, 
+      difficulty: '',
+      // 其他字段...
+    ),
+  ];
+
+  @override
+  Future<List<ChallengeModel>> getAllChallenges() async {
+    await Future.delayed(Duration(seconds: 1)); // 模拟网络延迟
+    return _mockData;
+  }
+  
+  @override
+  Future<ChallengeModel> addChallenge(ChallengeModel challenge) {
+    // TODO: implement addChallenge
+    throw UnimplementedError();
+  }
+  
+  @override
+  Future<void> deleteChallenge(int id) {
+    // TODO: implement deleteChallenge
+    throw UnimplementedError();
+  }
+  
+  @override
+  Future<ChallengeModel?> getChallengeById(int id) async{
+    await Future.delayed(Duration(seconds: 1)); // 模拟网络延迟
+    return _mockData[0];
+  }
+  
+  
+  @override
+  Future<ChallengeModel> updateChallenge(ChallengeModel challenge) {
+    // TODO: implement updateChallenge
+    throw UnimplementedError();
+  }
+
+  // 其他方法实现...
+}

+ 97 - 0
lib/core/repositories/sqlite_challenge_repository.dart

@@ -0,0 +1,97 @@
+import 'package:japp_flutter/core/models/challenge_model.dart';
+import 'package:japp_flutter/core/repositories/challenge_repository.dart';
+import 'package:japp_flutter/data/local/database_helper.dart';
+import 'package:sqflite/sqflite.dart';
+
+class SqliteChallengeRepository implements ChallengeRepository {
+  final DatabaseHelper dbHelper;
+
+  SqliteChallengeRepository(this.dbHelper);
+
+  @override
+  Future<List<ChallengeModel>> getAllChallenges() async {
+    final db = await dbHelper.database;
+    final maps = await db.query(DatabaseHelper.chllangetTable);
+    return maps.map(_mapDatabaseToModel).toList();
+  }
+
+  @override
+  Future<ChallengeModel> addChallenge(ChallengeModel challenge) async {
+    final db = await dbHelper.database;
+    await db.insert(
+      DatabaseHelper.chllangetTable,
+      _mapModelToDatabase(challenge),
+      conflictAlgorithm: ConflictAlgorithm.replace,
+    );
+    return challenge;
+  }
+
+  // 其他方法实现类似...
+
+  // Helper: 数据库行转 Model
+  ChallengeModel _mapDatabaseToModel(Map<String, dynamic> map) {
+    return ChallengeModel(
+      id: map[DatabaseHelper.columnId],
+      title: map[DatabaseHelper.columnTitle],
+      description: map['description'],
+      startDate: DateTime.fromMillisecondsSinceEpoch(map['startDate']),
+      endDate: DateTime.fromMillisecondsSinceEpoch(map['endDate']),
+      participants: map['participants'],
+      completed: map['completed'] == 1,
+      difficulty: map['difficulty'],
+    );
+  }
+
+  // Helper: Model 转数据库行
+  Map<String, dynamic> _mapModelToDatabase(ChallengeModel model) {
+    return {
+      DatabaseHelper.columnId: model.id,
+      DatabaseHelper.columnTitle: model.title,
+      'description': model.description,
+      'startDate': model.startDate.millisecondsSinceEpoch,
+      'endDate': model.endDate.millisecondsSinceEpoch,
+      'participants': model.participants,
+      'completed': model.completed ? 1 : 0,
+      'difficulty': model.difficulty,
+    };
+  }
+
+  @override
+  Future<void> deleteChallenge(int id) async {
+    final db = await dbHelper.database;
+    await db.delete(
+      DatabaseHelper.chllangetTable,
+      where: '${DatabaseHelper.columnId} = ?',
+      whereArgs: [id],
+    );
+  }
+
+  @override
+  Future<ChallengeModel?> getChallengeById(int id) async {
+    final db = await dbHelper.database;
+    final maps = await db.query(
+      DatabaseHelper.chllangetTable,
+      where: '${DatabaseHelper.columnId} = ?',
+      whereArgs: [id],
+      limit: 1,
+    );
+
+    if (maps.isNotEmpty) {
+      return _mapDatabaseToModel(maps.first);
+    }
+    return null;
+  }
+
+  @override
+  Future<ChallengeModel> updateChallenge(ChallengeModel challenge) async{
+    final db = await dbHelper.database;
+    await db.update(
+      DatabaseHelper.chllangetTable,
+      _mapModelToDatabase(challenge),
+      where: '${DatabaseHelper.columnId} = ?',
+      whereArgs: [challenge.id],
+      conflictAlgorithm: ConflictAlgorithm.replace,
+    );
+    return challenge;
+  }
+}

+ 43 - 0
lib/core/routes/app_router.dart

@@ -0,0 +1,43 @@
+import 'package:flutter/material.dart';
+import 'package:japp_flutter/core/models/challenge_model.dart';
+import 'package:japp_flutter/features/challenge/views/add_challenge_screen.dart';
+import 'package:japp_flutter/features/challenge/views/challenge_detail_screen.dart';
+import 'package:japp_flutter/features/challenge/views/challenge_list_screen.dart';
+import 'package:japp_flutter/features/challenge/views/edit_challenge_screen.dart';
+
+class AppRouter {
+  static const String challengeList = '/';
+  static const String challengeDetail = '/challenge/detail';
+  static const String addChallenge = '/challenge/add';
+  static const String editChallenge = '/challenge/edit';
+
+  static Route<dynamic> generateRoute(RouteSettings settings) {
+    switch (settings.name) {
+      case challengeList:
+        return MaterialPageRoute(builder: (_) => const ChallengeListScreen());
+      case challengeDetail:
+        final challengeId = settings.arguments as int;
+        return MaterialPageRoute(
+          builder: (_) => ChallengeDetailScreen(challengeId: challengeId),
+        );
+      case addChallenge:
+        return MaterialPageRoute(builder: (_) => const AddChallengeScreen());
+      case editChallenge:
+        final challenge = settings.arguments as ChallengeModel;
+        return MaterialPageRoute(
+          builder: (_) => ChallengeEditScreen(initialChallenge: challenge),
+        );
+      default:
+        return MaterialPageRoute(
+          builder: (_) => Scaffold(
+            body: Center(child: Text('No route defined for ${settings.name}')),
+          ),
+        );
+    }
+  }
+
+  // 静态跳转方法(可选)
+  static void navigateTo(BuildContext context, String routeName, {Object? args}) {
+    Navigator.pushNamed(context, routeName, arguments: args);
+  }
+}

+ 0 - 0
lib/core/services/challenge_service.dart


+ 44 - 0
lib/data/local/database_helper.dart

@@ -0,0 +1,44 @@
+import 'package:sqflite/sqflite.dart';
+import 'package:path/path.dart';
+
+class DatabaseHelper {
+  static const _databaseName = 'jay_world_app.db';
+  static const _databaseVersion = 1;
+
+  static const chllangetTable = 'challenges';
+  static const columnId = 'id';
+  static const columnTitle = 'title';
+  // 其他字段...
+
+  static Database? _database;
+
+  Future<Database> get database async {
+    if (_database != null) return _database!;
+    _database = await _initDatabase();
+    return _database!;
+  }
+
+  Future<Database> _initDatabase() async {
+    final path = join(await getDatabasesPath(), _databaseName);
+    return await openDatabase(
+      path,
+      version: _databaseVersion,
+      onCreate: _onCreate,
+    );
+  }
+
+  Future<void> _onCreate(Database db, int version) async {
+    await db.execute('''
+      CREATE TABLE $chllangetTable (
+        $columnId INTEGER PRIMARY KEY AUTOINCREMENT,
+        $columnTitle TEXT NOT NULL,
+        description TEXT NOT NULL,
+        startDate INTEGER NOT NULL,
+        endDate INTEGER NOT NULL,
+        participants INTEGER DEFAULT 0,
+        completed INTEGER DEFAULT 0,
+        difficulty TEXT NOT NULL
+      );
+    ''');
+  }
+}

+ 59 - 0
lib/features/challenge/view_models/challenge_add_vm.dart

@@ -0,0 +1,59 @@
+// features/challenge/add/add_challenge_viewmodel.dart
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:japp_flutter/core/models/challenge_model.dart';
+import 'package:japp_flutter/core/proviers/repository_providers.dart';
+import 'package:japp_flutter/core/repositories/challenge_repository.dart';
+
+final addChallengeProvider = AsyncNotifierProvider<AddChallengeViewModel, ChallengeModel>(
+  AddChallengeViewModel.new,
+);
+
+class AddChallengeViewModel extends AsyncNotifier<ChallengeModel> {
+  ChallengeRepository get _repository => ref.read(challengeRepositoryProvider);
+
+  @override
+  Future<ChallengeModel> build() async {
+    // 初始化为空挑战模板
+    return ChallengeModel(
+      id: null,
+      title: '',
+      description: '',
+      startDate: DateTime.now(),
+      endDate: DateTime.now().add(const Duration(days: 30)),
+      participants: 0,
+      completed: false,
+      difficulty: '中等',
+    );
+  }
+
+  /// 更新挑战标题
+  void updateTitle(String title) {
+    state = AsyncData(state.value!.copyWith(title: title));
+  }
+
+  /// 更新挑战描述
+  void updateDescription(String description) {
+    state = AsyncData(state.value!.copyWith(description: description));
+  }
+
+  /// 更新难度级别
+  void updateDifficulty(String difficulty) {
+    state = AsyncData(state.value!.copyWith(difficulty: difficulty));
+  }
+
+  /// 更新日期范围
+  void updateDateRange(DateTime start, DateTime end) {
+    state = AsyncData(state.value!.copyWith(
+      startDate: start,
+      endDate: end,
+    ));
+  }
+
+  /// 提交新挑战
+  Future<void> submitChallenge() async {
+    if (state.value == null) return;
+
+    state = const AsyncLoading();
+    state = await AsyncValue.guard(() => _repository.addChallenge(state.value!));
+  }
+}

+ 30 - 0
lib/features/challenge/view_models/challenge_detail_vm.dart

@@ -0,0 +1,30 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:japp_flutter/core/models/challenge_model.dart';
+import 'package:japp_flutter/core/proviers/repository_providers.dart';
+import 'package:japp_flutter/core/repositories/challenge_repository.dart';
+
+
+
+final challengeDetailProvider = AsyncNotifierProvider.autoDispose<ChallengeDetailViewModel, ChallengeModel?>(
+  ChallengeDetailViewModel.new,
+);
+
+
+class ChallengeDetailViewModel extends AutoDisposeAsyncNotifier<ChallengeModel?> {
+  ChallengeRepository get _repository => ref.read(challengeRepositoryProvider);
+
+  @override
+  Future<ChallengeModel?> build() async {
+    return null;
+  }
+
+  Future<void> loadChallenge(int challengeId) async {
+    state = const AsyncLoading();
+    state = await AsyncValue.guard(() => _repository.getChallengeById(challengeId));
+  }
+
+  // 手动刷新用
+  Future<void> refreshChallenge(int challengeId) async {
+    await loadChallenge(challengeId);
+  }
+}

+ 53 - 0
lib/features/challenge/view_models/challenge_edit_vm.dart

@@ -0,0 +1,53 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:japp_flutter/core/models/challenge_model.dart';
+import 'package:japp_flutter/core/proviers/repository_providers.dart';
+import 'package:japp_flutter/core/repositories/challenge_repository.dart';
+
+final challengeEditProvider = AsyncNotifierProvider.autoDispose<ChallengeEditViewModel, ChallengeModel?>(
+  ChallengeEditViewModel.new,
+);
+
+class ChallengeEditViewModel extends AutoDisposeAsyncNotifier<ChallengeModel?> {
+  ChallengeRepository get _repository => ref.read(challengeRepositoryProvider);
+
+  @override
+  Future<ChallengeModel?> build() async {
+    // 初始化为 null,由外部传入初始数据
+    return null;
+  }
+
+  /// 初始化编辑数据(从详情页传入)
+  void initialize(ChallengeModel challenge) {
+    state = AsyncData(challenge);
+  }
+
+  /// 更新挑战标题
+  void updateTitle(String title) {
+    state.whenData((challenge) {
+      state = AsyncData(challenge!.copyWith(title: title));
+    });
+  }
+
+  /// 更新挑战描述
+  void updateDescription(String description) {
+    state.whenData((challenge) {
+      state = AsyncData(challenge!.copyWith(description: description));
+    });
+  }
+
+  /// 更新难度
+  void updateDifficulty(String difficulty) {
+    state.whenData((challenge) {
+      state = AsyncData(challenge!.copyWith(difficulty: difficulty));
+    });
+  }
+
+  /// 保存挑战
+  Future<void> saveChallenge() async {
+    final challenge = state.value;
+    if (challenge == null) return;
+
+    state = const AsyncLoading();
+    state = await AsyncValue.guard(() => _repository.updateChallenge(challenge));
+  }
+}

+ 28 - 0
lib/features/challenge/view_models/challenge_list_vm.dart

@@ -0,0 +1,28 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:japp_flutter/core/models/challenge_model.dart';
+import 'package:japp_flutter/core/proviers/repository_providers.dart';
+import 'package:japp_flutter/core/repositories/challenge_repository.dart';
+
+
+final challengeListProvider = AsyncNotifierProvider<ChallengeListViewModel, List<ChallengeModel>>(
+  ChallengeListViewModel.new,
+);
+
+class ChallengeListViewModel extends AsyncNotifier<List<ChallengeModel>> {
+  ChallengeRepository get _repository => ref.read(challengeRepositoryProvider);
+
+  @override
+  Future<List<ChallengeModel>> build() async {
+    // 初始加载数据(可选)
+    return []; // 初始化为空列表,或直接调用 loadChallenges()
+  }
+
+  /// 加载挑战列表(自动处理加载/错误状态)
+  Future<void> loadChallenges() async {
+    state = const AsyncLoading();
+    state = await AsyncValue.guard(() => _repository.getAllChallenges());
+  }
+
+  /// 刷新数据(别名方法)
+  Future<void> refresh() async => loadChallenges();
+}

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

@@ -1,74 +0,0 @@
-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();
-    }
-  }
-}

+ 197 - 0
lib/features/challenge/views/add_challenge_screen.dart

@@ -0,0 +1,197 @@
+// features/challenge/add/add_challenge_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_add_vm.dart';
+
+class AddChallengeScreen extends ConsumerWidget {
+  const AddChallengeScreen({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final challengeState = ref.watch(addChallengeProvider);
+    final theme = Theme.of(context);
+
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('创建新挑战'),
+        actions: [
+          IconButton(
+            icon: const Icon(Icons.check),
+            onPressed: () async {
+              await ref.read(addChallengeProvider.notifier).submitChallenge();
+              if (context.mounted) Navigator.pop(context);
+            },
+            tooltip: '提交',
+          ),
+        ],
+      ),
+      body: challengeState.when(
+        loading: () => const Center(child: CircularProgressIndicator()),
+        error: (error, _) => Center(child: Text('错误: $error')),
+        data: (challenge) => _BuildForm(challenge: challenge),
+      ),
+    );
+  }
+}
+
+class _BuildForm extends ConsumerWidget {
+  final ChallengeModel challenge;
+
+  const _BuildForm({required this.challenge});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return SingleChildScrollView(
+      padding: const EdgeInsets.all(16),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          _TitleField(initialValue: challenge.title),
+          const SizedBox(height: 24),
+          _DateRangeField(
+            startDate: challenge.startDate,
+            endDate: challenge.endDate,
+          ),
+          const SizedBox(height: 24),
+          _DifficultySelector(currentDifficulty: challenge.difficulty),
+          const SizedBox(height: 24),
+          _DescriptionField(initialValue: challenge.description),
+        ],
+      ),
+    );
+  }
+}
+
+class _TitleField extends ConsumerWidget {
+  final String initialValue;
+
+  const _TitleField({required this.initialValue});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return TextFormField(
+      initialValue: initialValue,
+      decoration: const InputDecoration(
+        labelText: '挑战标题*',
+        border: OutlineInputBorder(),
+      ),
+      onChanged: (value) => ref.read(addChallengeProvider.notifier).updateTitle(value),
+    );
+  }
+}
+
+class _DescriptionField extends ConsumerWidget {
+  final String initialValue;
+
+  const _DescriptionField({required this.initialValue});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return TextFormField(
+      initialValue: initialValue,
+      decoration: const InputDecoration(
+        labelText: '挑战描述',
+        border: OutlineInputBorder(),
+        alignLabelWithHint: true,
+      ),
+      maxLines: 5,
+      onChanged: (value) => ref.read(addChallengeProvider.notifier).updateDescription(value),
+    );
+  }
+}
+
+class _DifficultySelector extends ConsumerWidget {
+  final String currentDifficulty;
+  static const difficulties = ['简单', '中等', '困难'];
+
+  const _DifficultySelector({required this.currentDifficulty});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        const Text('难度级别*', style: TextStyle(fontSize: 16)),
+        const SizedBox(height: 8),
+        Wrap(
+          spacing: 8,
+          children: difficulties.map((level) {
+            return ChoiceChip(
+              label: Text(level),
+              selected: currentDifficulty == level,
+              onSelected: (selected) {
+                if (selected) {
+                  ref.read(addChallengeProvider.notifier).updateDifficulty(level);
+                }
+              },
+            );
+          }).toList(),
+        ),
+      ],
+    );
+  }
+}
+
+class _DateRangeField extends ConsumerWidget {
+  final DateTime startDate;
+  final DateTime endDate;
+
+  const _DateRangeField({
+    required this.startDate,
+    required this.endDate,
+  });
+
+  Future<void> _selectDate(BuildContext context, bool isStartDate, WidgetRef ref) async {
+    final initialDate = isStartDate ? startDate : endDate;
+    final picked = await showDatePicker(
+      context: context,
+      initialDate: initialDate,
+      firstDate: DateTime.now(),
+      lastDate: DateTime(2100),
+    );
+
+    if (picked != null) {
+      final newStart = isStartDate ? picked : startDate;
+      final newEnd = isStartDate ? endDate : picked;
+      ref.read(addChallengeProvider.notifier).updateDateRange(newStart, newEnd);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        const Text('挑战周期*', style: TextStyle(fontSize: 16)),
+        const SizedBox(height: 8),
+        Row(
+          children: [
+            Expanded(
+              child: OutlinedButton(
+                onPressed: () => _selectDate(context, true, ref),
+                child: Text(DateFormat('yyyy/MM/dd').format(startDate)),
+              ),
+            ),
+            const Padding(
+              padding: EdgeInsets.symmetric(horizontal: 8),
+              child: Text('至'),
+            ),
+            Expanded(
+              child: OutlinedButton(
+                onPressed: () => _selectDate(context, false, ref),
+                child: Text(DateFormat('yyyy/MM/dd').format(endDate)),
+              ),
+            ),
+          ],
+        ),
+        const SizedBox(height: 4),
+        Text(
+          '总天数: ${endDate.difference(startDate).inDays}天',
+          style: const TextStyle(color: Colors.grey),
+        ),
+      ],
+    );
+  }
+}

+ 141 - 0
lib/features/challenge/views/challenge_detail_screen.dart

@@ -0,0 +1,141 @@
+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_detail_vm.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+class ChallengeDetailScreen extends ConsumerWidget {
+  final int challengeId;
+
+  const ChallengeDetailScreen({super.key, required this.challengeId});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final challengeAsync = ref.watch(challengeDetailProvider);
+
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text("挑战详情"),
+        actions: [
+          IconButton(
+            icon: const Icon(Icons.edit),
+            onPressed: () {
+              final challenge = challengeAsync.value;
+              if (challenge != null) {
+                Navigator.pushNamed(context, '/edit/${challenge.id}');
+              }
+            },
+          ),
+        ],
+      ),
+      body: switch (challengeAsync) {
+        AsyncError(:final error) => Center(child: Text('加载失败: $error')),
+        AsyncData(value: final value) => _buildChallengeContent(value!),
+        _ => const Center(child: CircularProgressIndicator()),
+
+      },
+    );
+  }
+
+  Widget _buildChallengeContent(ChallengeModel challenge) {
+    return SingleChildScrollView(
+      padding: const EdgeInsets.all(16),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(
+            challenge.title,
+            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+          ),
+          const SizedBox(height: 8),
+          Chip(
+            label: Text(
+              challenge.difficulty,
+              style: const TextStyle(color: Colors.white),
+            ),
+            backgroundColor: _getDifficultyColor(challenge.difficulty),
+          ),
+          const SizedBox(height: 16),
+          Text(
+            challenge.description,
+            style: const TextStyle(fontSize: 16, height: 1.5),
+          ),
+          const SizedBox(height: 24),
+          _buildDateInfo('开始时间', challenge.startDate),
+          _buildDateInfo('结束时间', challenge.endDate),
+          const SizedBox(height: 16),
+          _buildParticipantInfo(challenge.participants),
+          const SizedBox(height: 24),
+          _buildProgressSection(challenge),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildDateInfo(String label, DateTime date) {
+    return Padding(
+      padding: const EdgeInsets.only(bottom: 8),
+      child: Row(
+        children: [
+          Text('$label: ', style: const TextStyle(fontWeight: FontWeight.bold)),
+          Text(DateFormat('yyyy年MM月dd日').format(date)),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildParticipantInfo(int count) {
+    return Row(
+      children: [
+        const Icon(Icons.people, size: 20),
+        const SizedBox(width: 8),
+        Text('$count 人参与'),
+      ],
+    );
+  }
+
+  Widget _buildProgressSection(ChallengeModel challenge) {
+    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(fontWeight: FontWeight.bold),
+        ),
+        const SizedBox(height: 8),
+        LinearProgressIndicator(
+          value: progress,
+          minHeight: 10,
+          borderRadius: BorderRadius.circular(5),
+          color: Colors.blue,
+          backgroundColor: Colors.blue.shade100,
+        ),
+        const SizedBox(height: 8),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: [
+            Text('已进行 $passedDays 天'),
+            Text('剩余 ${challenge.remainingDays} 天'),
+          ],
+        ),
+      ],
+    );
+  }
+
+  Color _getDifficultyColor(String difficulty) {
+    switch (difficulty) {
+      case '简单':
+        return Colors.green;
+      case '中等':
+        return Colors.orange;
+      case '困难':
+        return Colors.red;
+      default:
+        return Colors.grey;
+    }
+  }
+}

+ 231 - 0
lib/features/challenge/views/challenge_list_screen.dart

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

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

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

+ 107 - 0
lib/features/challenge/views/edit_challenge_screen.dart

@@ -0,0 +1,107 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:japp_flutter/core/models/challenge_model.dart';
+import 'package:japp_flutter/features/challenge/view_models/challenge_edit_vm.dart';
+
+class ChallengeEditScreen extends ConsumerWidget {
+  final ChallengeModel? initialChallenge;
+
+  const ChallengeEditScreen({
+    super.key,
+    this.initialChallenge,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final challengeState = ref.watch(challengeEditProvider);
+    final theme = Theme.of(context);
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(initialChallenge == null ? '创建挑战' : '编辑挑战'),
+        actions: [
+          IconButton(
+            icon: const Icon(Icons.save),
+            onPressed: () async {
+              await ref.read(challengeEditProvider.notifier).saveChallenge();
+              if (context.mounted) Navigator.pop(context);
+            },
+          ),
+        ],
+      ),
+      body: challengeState.when(
+        loading: () => const Center(child: CircularProgressIndicator()),
+        error: (error, _) => Center(child: Text('错误: $error')),
+        data: (challenge) {
+          if (challenge == null) {
+            return const Center(child: Text('未初始化数据'));
+          }
+
+          return SingleChildScrollView(
+            padding: const EdgeInsets.all(16),
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                _buildTitleField(challenge, ref),
+                const SizedBox(height: 24),
+                _buildDifficultySelector(challenge, ref),
+                const SizedBox(height: 24),
+                _buildDescriptionField(challenge, ref),
+              ],
+            ),
+          );
+        },
+      ),
+    );
+  }
+
+  Widget _buildTitleField(ChallengeModel challenge, WidgetRef ref) {
+    return TextFormField(
+      initialValue: challenge.title,
+      decoration: const InputDecoration(
+        labelText: '挑战标题',
+        border: OutlineInputBorder(),
+      ),
+      onChanged: (value) => ref.read(challengeEditProvider.notifier).updateTitle(value),
+    );
+  }
+
+  Widget _buildDescriptionField(ChallengeModel challenge, WidgetRef ref) {
+    return TextFormField(
+      initialValue: challenge.description,
+      decoration: const InputDecoration(
+        labelText: '挑战描述',
+        border: OutlineInputBorder(),
+        alignLabelWithHint: true,
+      ),
+      maxLines: 5,
+      onChanged: (value) => ref.read(challengeEditProvider.notifier).updateDescription(value),
+    );
+  }
+
+  Widget _buildDifficultySelector(ChallengeModel challenge, WidgetRef ref) {
+    const difficulties = ['简单', '中等', '困难'];
+    
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        const Text('难度级别', style: TextStyle(fontSize: 16)),
+        const SizedBox(height: 8),
+        Wrap(
+          spacing: 8,
+          children: difficulties.map((level) {
+            return ChoiceChip(
+              label: Text(level),
+              selected: challenge.difficulty == level,
+              onSelected: (selected) {
+                if (selected) {
+                  ref.read(challengeEditProvider.notifier).updateDifficulty(level);
+                }
+              },
+            );
+          }).toList(),
+        ),
+      ],
+    );
+  }
+}

+ 6 - 9
lib/main.dart

@@ -1,9 +1,8 @@
 import 'package:flutter/material.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';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:japp_flutter/core/routes/app_router.dart';
 
-void main() => runApp(const ChallengeApp());
+void main() => runApp(ProviderScope(child: const ChallengeApp()));
 
 class ChallengeApp extends StatelessWidget {
   const ChallengeApp({super.key});
@@ -17,10 +16,8 @@ class ChallengeApp extends StatelessWidget {
         primarySwatch: Colors.blue,
         appBarTheme: const AppBarTheme(elevation: 1),
       ),
-      home: ChangeNotifierProvider(
-        create: (_) => ChallengeListViewModel(),
-        child: const ChallengeListScreen(),
-      ),
+      initialRoute: AppRouter.challengeList,
+      onGenerateRoute: AppRouter.generateRoute,
     );
   }
-}
+}

+ 2 - 0
macos/Flutter/GeneratedPluginRegistrant.swift

@@ -5,6 +5,8 @@
 import FlutterMacOS
 import Foundation
 
+import sqflite_darwin
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+  SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
 }

+ 97 - 1
pubspec.lock

@@ -78,6 +78,14 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_hooks:
+    dependency: "direct main"
+    description:
+      name: flutter_hooks
+      sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.21.2"
   flutter_lints:
     dependency: "direct dev"
     description:
@@ -86,6 +94,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "5.0.0"
+  flutter_riverpod:
+    dependency: "direct main"
+    description:
+      name: flutter_riverpod
+      sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.6.1"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -179,6 +195,22 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.9.1"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.1.6"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.8"
   provider:
     dependency: "direct main"
     description:
@@ -187,6 +219,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "6.1.5"
+  riverpod:
+    dependency: transitive
+    description:
+      name: riverpod
+      sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.6.1"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -200,6 +240,46 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.10.1"
+  sqflite:
+    dependency: "direct main"
+    description:
+      name: sqflite
+      sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.4.2"
+  sqflite_android:
+    dependency: transitive
+    description:
+      name: sqflite_android
+      sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.4.1"
+  sqflite_common:
+    dependency: transitive
+    description:
+      name: sqflite_common
+      sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.5.5"
+  sqflite_darwin:
+    dependency: transitive
+    description:
+      name: sqflite_darwin
+      sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.4.2"
+  sqflite_platform_interface:
+    dependency: transitive
+    description:
+      name: sqflite_platform_interface
+      sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.4.0"
   stack_trace:
     dependency: transitive
     description:
@@ -208,6 +288,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.12.1"
+  state_notifier:
+    dependency: transitive
+    description:
+      name: state_notifier
+      sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0"
   stream_channel:
     dependency: transitive
     description:
@@ -224,6 +312,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.4.1"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.4.0"
   term_glyph:
     dependency: transitive
     description:
@@ -274,4 +370,4 @@ packages:
     version: "1.1.1"
 sdks:
   dart: ">=3.8.1 <4.0.0"
-  flutter: ">=3.18.0-18.0.pre.54"
+  flutter: ">=3.24.0"

+ 3 - 0
pubspec.yaml

@@ -37,6 +37,9 @@ dependencies:
   intl: ^0.20.2
   provider: ^6.0.0 # 核心状态管理
   dio: ^5.0.0       # 网络请求
+  sqflite: ^2.4.2
+  flutter_hooks: ^0.21.2
+  flutter_riverpod: ^2.6.1
   
 dev_dependencies:
   flutter_test: