ソースを参照

本commit主要提交下界面相关变更,提交了一些界面相关的 demo 代码

ValiZhang 9 ヶ月 前
コミット
a1a0f46f0f

+ 35 - 0
lib/core/constants/options.dart

@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+import 'package:japp_flutter/core/models/option_model.dart';
+import 'package:japp_flutter/core/models/dropdown_item.dart';
+
+// 通用状态选项
+const List<OptionModel<String>> statusOptions = [
+  OptionModel(value: 'active', label: '激活', icon: Icons.check_circle),
+  OptionModel(value: 'inactive', label: '未激活', icon: Icons.pause_circle),
+  OptionModel(value: 'archived', label: '已归档', icon: Icons.archive),
+];
+
+// 通用优先级选项
+const List<OptionModel<int>> priorityOptions = [
+  OptionModel(value: 1, label: '低'),
+  OptionModel(value: 2, label: '中'),
+  OptionModel(value: 3, label: '高'),
+];
+
+// 通用优先级选项
+const List<OptionModel<int>> difficultOptions = [
+  OptionModel(value: 1, label: '简单'),
+  OptionModel(value: 2, label: '普通'),
+  OptionModel(value: 3, label: '困难'),
+  OptionModel(value: 4, label: '地狱'),
+];
+
+
+
+// 挑战目标难度选项
+const List<DropdownItem<int>> challengeDifficultyOpts = [
+  DropdownItem(value: 1, label: '简单', icon: Icons.sentiment_very_satisfied),
+  DropdownItem(value: 2, label: '普通',icon: Icons.sentiment_satisfied),
+  DropdownItem(value: 3, label: '困难',icon:Icons.sentiment_dissatisfied),
+  DropdownItem(value: 4, label: '地狱',icon: Icons.sentiment_very_dissatisfied),
+];

+ 58 - 34
lib/core/models/challenge_model.dart

@@ -1,74 +1,98 @@
 import 'package:flutter/foundation.dart';
 
-@immutable // 标记为不可变类
+// 待办
+@immutable
 class ChallengeModel {
   final int? id;
+  final int actionType; // 1-愿景 2-战略目标 3-挑战目标 4-待办目标
   final String title;
   final String description;
-  final DateTime startDate;
-  final DateTime endDate;
-  final int participants;
-  final bool completed;
-  final String difficulty;
-  final int? remainingDays; // 改为可选字段
+  final String cover;
+  final int sort;
+  final int status; // 1-正常 2-进行中 3-已完成 4-失败放弃
+  final int difficulty; // 1-简单 2-正常 3-困难 4-地狱
+  final String remark;
+  final DateTime? startDate;
+  final DateTime? endDate;
+  final DateTime? finishDate;
+  final DateTime? planFinishDate;
+  final int parentId;
 
   const ChallengeModel({
     required this.id,
+    required this.actionType,
     required this.title,
     required this.description,
+    required this.cover,
+    required this.sort,
+    required this.status,
+    required this.difficulty,
+    required this.remark,
     required this.startDate,
     required this.endDate,
-    required this.participants,
-    required this.completed,
-    required this.difficulty,
-    this.remainingDays, // 可选计算字段
+    required this.finishDate,
+    required this.parentId,
+    required this.planFinishDate,
   });
 
-  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,
+      startDate: DateTime.parse(json['start_date'] as String),
+      endDate: DateTime.parse(json['end_date'] as String),
+      cover: json['cover'] as String,
+      sort: json['sort'] as int,
+      status: json['status'] as int,
+      remark: json['remark'] as String,
+      finishDate: DateTime.parse(json['finish_date'] as String),
+      parentId: json['parent_id'] as int, 
+      actionType: 0, 
+      difficulty: 0, 
+      planFinishDate: DateTime.parse(json['end_date'] 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,
-  };
+  // Map<String, dynamic> toJson() => {
+  //   if (id != null) 'id': id,
+  //   'title': title,
+  //   'description': description,
+  //   'startDate': startDate.toIso8601String(),
+  //   'endDate': endDate.toIso8601String(),
+  // };
 
   ChallengeModel copyWith({
     int? id,
+    int? actionType,
     String? title,
     String? description,
+    int? status,
+    int? sort,
+    String? cover,
+    String? remark,
+    int? difficulty,
     DateTime? startDate,
     DateTime? endDate,
-    int? participants,
-    bool? completed,
-    String? difficulty,
+    DateTime? finishDate,
+    DateTime? planFinishDate,
+    int? parentId,
   }) {
     return ChallengeModel(
       id: id ?? this.id,
+      actionType: actionType ?? this.actionType,
       title: title ?? this.title,
       description: description ?? this.description,
       startDate: startDate ?? this.startDate,
-      endDate: endDate ?? this.endDate,
-      participants: participants ?? this.participants,
-      completed: completed ?? this.completed,
+      endDate: endDate ?? this.endDate, 
+      cover: cover ?? this.cover, 
+      sort: sort ?? this.sort, 
+      status: status ?? this.status, 
+      remark: remark ?? this.remark,
+      finishDate: finishDate ?? this.finishDate,
+      planFinishDate: planFinishDate ?? this.planFinishDate,
+      parentId: parentId ?? this.parentId,
       difficulty: difficulty ?? this.difficulty,
     );
   }

+ 16 - 0
lib/core/models/dropdown_item.dart

@@ -0,0 +1,16 @@
+import 'package:flutter/material.dart';
+
+
+class DropdownItem<T> {
+  final T value;
+  final String label;
+  final IconData? icon;
+  final Color? iconColor;
+
+  const DropdownItem({
+    required this.value,
+    required this.label,
+    this.icon,
+    this.iconColor,
+  });
+}

+ 23 - 0
lib/core/models/option_model.dart

@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+
+class OptionModel<T> {
+  final T value;
+  final String label;
+  final IconData? icon;
+  
+  const OptionModel({
+    required this.value,
+    required this.label,
+    this.icon,
+  });
+  
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is OptionModel &&
+          runtimeType == other.runtimeType &&
+          value == other.value;
+  
+  @override
+  int get hashCode => value.hashCode;
+}

+ 6 - 1
lib/core/repositories/challenge_repository.dart

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

+ 7 - 4
lib/core/repositories/mock_challenge_repository.dart

@@ -10,15 +10,17 @@ class MockChallengeRepository implements ChallengeRepository {
       startDate: DateTime.now(),
       endDate: DateTime.now().add(Duration(days: 30)), 
       description: '', 
-      participants: 1, 
-      completed: false, 
-      difficulty: '',
+      difficulty: 1, actionType: 1, cover: '', sort: 1, status: 1, remark: '', finishDate: DateTime.now(), parentId: 0, planFinishDate: DateTime.now(),
       // 其他字段...
     ),
   ];
 
   @override
-  Future<List<ChallengeModel>> getAllChallenges() async {
+  Future<List<ChallengeModel>> getChallenges({
+        int page=1,
+    int pageSize = 20,
+    int actionType= 1,
+  }) async {
     await Future.delayed(Duration(seconds: 1)); // 模拟网络延迟
     return _mockData;
   }
@@ -47,6 +49,7 @@ class MockChallengeRepository implements ChallengeRepository {
     // TODO: implement updateChallenge
     throw UnimplementedError();
   }
+  
 
   // 其他方法实现...
 }

+ 34 - 10
lib/core/repositories/sqlite_challenge_repository.dart

@@ -9,7 +9,11 @@ class SqliteChallengeRepository implements ChallengeRepository {
   SqliteChallengeRepository(this.dbHelper);
 
   @override
-  Future<List<ChallengeModel>> getAllChallenges() async {
+  Future<List<ChallengeModel>> getChallenges({
+    int page = 1,
+    int pageSize = 20,
+    int actionType = 1,
+  }) async {
     final db = await dbHelper.database;
     final maps = await db.query(DatabaseHelper.chllangetTable);
     return maps.map(_mapDatabaseToModel).toList();
@@ -34,11 +38,25 @@ class SqliteChallengeRepository implements ChallengeRepository {
       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,
+      actionType: map['action_type'],
+      cover: map['cover'],
+      sort: map['sort'],
+      status: map['status'],
+      remark: map['remark'],
+      parentId: map['parent_id'],
       difficulty: map['difficulty'],
+      finishDate: map['finish_date']
+          ? DateTime.fromMillisecondsSinceEpoch(map['finish_date'])
+          : null,
+      planFinishDate: map['plan_finish_date']
+          ? DateTime.fromMillisecondsSinceEpoch(map['plan_finish_date'])
+          : null,
+      startDate: map['start_date']
+          ? DateTime.fromMillisecondsSinceEpoch(map['start_date'])
+          : null,
+      endDate: map['end_date']
+          ? DateTime.fromMillisecondsSinceEpoch(map['end_date'])
+          : null,
     );
   }
 
@@ -47,12 +65,18 @@ class SqliteChallengeRepository implements ChallengeRepository {
     return {
       DatabaseHelper.columnId: model.id,
       DatabaseHelper.columnTitle: model.title,
+      'action_type':model.actionType,
+      'cover':model.cover,
+      'sort':model.sort,
+      'status':model.status,
+      'remark':model.remark,
+      'parent_id':model.parentId,
       'description': model.description,
-      'startDate': model.startDate.millisecondsSinceEpoch,
-      'endDate': model.endDate.millisecondsSinceEpoch,
-      'participants': model.participants,
-      'completed': model.completed ? 1 : 0,
       'difficulty': model.difficulty,
+      'start_date': model.startDate?.millisecondsSinceEpoch,
+      'end_date': model.endDate?.millisecondsSinceEpoch,
+      'finish_date':model.finishDate,
+      'plan_finish_date':model.planFinishDate,
     };
   }
 
@@ -83,7 +107,7 @@ class SqliteChallengeRepository implements ChallengeRepository {
   }
 
   @override
-  Future<ChallengeModel> updateChallenge(ChallengeModel challenge) async{
+  Future<ChallengeModel> updateChallenge(ChallengeModel challenge) async {
     final db = await dbHelper.database;
     await db.update(
       DatabaseHelper.chllangetTable,

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

@@ -4,12 +4,14 @@ 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';
+import 'package:japp_flutter/features/challenge/views/mock_page.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 const String mockPage = '/mock/page';
 
   static Route<dynamic> generateRoute(RouteSettings settings) {
     switch (settings.name) {
@@ -27,6 +29,8 @@ class AppRouter {
         return MaterialPageRoute(
           builder: (_) => ChallengeEditScreen(initialChallenge: challenge),
         );
+      case mockPage:
+        return MaterialPageRoute(builder: (_)=> const MockPage());
       default:
         return MaterialPageRoute(
           builder: (_) => Scaffold(

+ 11 - 5
lib/data/local/database_helper.dart

@@ -32,12 +32,18 @@ class DatabaseHelper {
       CREATE TABLE $chllangetTable (
         $columnId INTEGER PRIMARY KEY AUTOINCREMENT,
         $columnTitle TEXT NOT NULL,
+        action_type INTEGER 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
+        cover TEXT NOT NULL,
+        sort INTEGER NOT NULL,
+        status INTEGER NOT NULL,
+        difficulty INTEGER NOT NULL,
+        remark TEXT NOT NULL,
+        start_date INTEGER,
+        end_date INTEGER,
+        finish_date INTEGER,
+        plan_finish_date INTEGER,
+        parent_id INTEGER NOT NULL
       );
     ''');
   }

+ 19 - 13
lib/features/challenge/view_models/challenge_add_vm.dart

@@ -4,9 +4,10 @@ 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,
-);
+final addChallengeProvider =
+    AsyncNotifierProvider<AddChallengeViewModel, ChallengeModel>(
+      AddChallengeViewModel.new,
+    );
 
 class AddChallengeViewModel extends AsyncNotifier<ChallengeModel> {
   ChallengeRepository get _repository => ref.read(challengeRepositoryProvider);
@@ -20,9 +21,15 @@ class AddChallengeViewModel extends AsyncNotifier<ChallengeModel> {
       description: '',
       startDate: DateTime.now(),
       endDate: DateTime.now().add(const Duration(days: 30)),
-      participants: 0,
-      completed: false,
-      difficulty: '中等',
+      actionType: 1,
+      cover: '',
+      sort: 200,
+      status: 1,
+      difficulty: 1,
+      remark: '',
+      finishDate: null,
+      parentId: 1,
+      planFinishDate: null
     );
   }
 
@@ -37,16 +44,13 @@ class AddChallengeViewModel extends AsyncNotifier<ChallengeModel> {
   }
 
   /// 更新难度级别
-  void updateDifficulty(String difficulty) {
+  void updateDifficulty(int difficulty) {
     state = AsyncData(state.value!.copyWith(difficulty: difficulty));
   }
 
   /// 更新日期范围
   void updateDateRange(DateTime start, DateTime end) {
-    state = AsyncData(state.value!.copyWith(
-      startDate: start,
-      endDate: end,
-    ));
+    state = AsyncData(state.value!.copyWith(startDate: start, endDate: end));
   }
 
   /// 提交新挑战
@@ -54,6 +58,8 @@ class AddChallengeViewModel extends AsyncNotifier<ChallengeModel> {
     if (state.value == null) return;
 
     state = const AsyncLoading();
-    state = await AsyncValue.guard(() => _repository.addChallenge(state.value!));
+    state = await AsyncValue.guard(
+      () => _repository.addChallenge(state.value!),
+    );
   }
-}
+}

+ 3 - 2
lib/features/challenge/view_models/challenge_edit_vm.dart

@@ -35,13 +35,14 @@ class ChallengeEditViewModel extends AutoDisposeAsyncNotifier<ChallengeModel?> {
     });
   }
 
-  /// 更新难度
-  void updateDifficulty(String difficulty) {
+  /// 更新挑战描述
+  void updateDifficulty(int difficulty) {
     state.whenData((challenge) {
       state = AsyncData(challenge!.copyWith(difficulty: difficulty));
     });
   }
 
+
   /// 保存挑战
   Future<void> saveChallenge() async {
     final challenge = state.value;

+ 1 - 1
lib/features/challenge/view_models/challenge_list_vm.dart

@@ -22,7 +22,7 @@ class ChallengeListViewModel extends AsyncNotifier<List<ChallengeModel>> {
   Future<List<ChallengeModel>> loadChallenges() async {
     state = const AsyncValue.loading();
     try {
-      final challenges = await _repository.getAllChallenges();
+      final challenges = await _repository.getChallenges();
       state = AsyncValue.data(challenges);
       return challenges;
     } catch (e, stack) {

+ 9 - 8
lib/features/challenge/views/add_challenge_screen.dart

@@ -2,6 +2,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:intl/intl.dart';
+import 'package:japp_flutter/core/constants/options.dart';
 import 'package:japp_flutter/core/models/challenge_model.dart';
 import 'package:japp_flutter/features/challenge/view_models/challenge_add_vm.dart';
 
@@ -51,8 +52,8 @@ class _BuildForm extends ConsumerWidget {
           _TitleField(initialValue: challenge.title),
           const SizedBox(height: 24),
           _DateRangeField(
-            startDate: challenge.startDate,
-            endDate: challenge.endDate,
+            startDate: challenge.startDate!,
+            endDate: challenge.endDate!,
           ),
           const SizedBox(height: 24),
           _DifficultySelector(currentDifficulty: challenge.difficulty),
@@ -103,8 +104,8 @@ class _DescriptionField extends ConsumerWidget {
 }
 
 class _DifficultySelector extends ConsumerWidget {
-  final String currentDifficulty;
-  static const difficulties = ['简单', '中等', '困难'];
+  final int currentDifficulty;
+  // static const difficulties = ['简单', '中等', '困难', '地狱'];
 
   const _DifficultySelector({required this.currentDifficulty});
 
@@ -117,13 +118,13 @@ class _DifficultySelector extends ConsumerWidget {
         const SizedBox(height: 8),
         Wrap(
           spacing: 8,
-          children: difficulties.map((level) {
+          children: difficultOptions.map((option) {
             return ChoiceChip(
-              label: Text(level),
-              selected: currentDifficulty == level,
+              label: Text(option.label),
+              selected: currentDifficulty == option.value,
               onSelected: (selected) {
                 if (selected) {
-                  ref.read(addChallengeProvider.notifier).updateDifficulty(level);
+                  ref.read(addChallengeProvider.notifier).updateDifficulty(option.value);
                 }
               },
             );

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

@@ -47,26 +47,11 @@ class ChallengeDetailScreen extends ConsumerWidget {
             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),
         ],
       ),
     );
@@ -94,48 +79,4 @@ class ChallengeDetailScreen extends ConsumerWidget {
     );
   }
 
-  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;
-    }
-  }
 }

+ 2 - 2
lib/features/challenge/views/challenge_list_screen.dart

@@ -3,7 +3,7 @@ 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_list_vm.dart';
-import 'package:japp_flutter/features/challenge/views/widgets/challenge_card.dart';
+import 'package:japp_flutter/features/challenge/views/widgets/challenge_list_card_widget.dart';
 
 class ChallengeListScreen extends ConsumerWidget {
   const ChallengeListScreen({super.key});
@@ -57,7 +57,7 @@ class ChallengeListScreen extends ConsumerWidget {
               child: ListView.builder(
                 padding: const EdgeInsets.all(16),
                 itemCount: challenges.length,
-                itemBuilder: (_, index) => ChallengeCard(challenge: challenges[index]),
+                itemBuilder: (_, index) => ChallengeListCard(challenge: challenges[index]),
                 
                 // itemBuilder: (context, index) => ListTile(
                 //   title: Text(challenges[index].title),

+ 4 - 2
lib/features/challenge/views/edit_challenge_screen.dart

@@ -80,7 +80,9 @@ class ChallengeEditScreen extends ConsumerWidget {
   }
 
   Widget _buildDifficultySelector(ChallengeModel challenge, WidgetRef ref) {
-    const difficulties = ['简单', '中等', '困难'];
+    // const difficulties = ['简单', '中等', '困难'];
+
+    const difficulties = [1,2,3,4];
     
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
@@ -91,7 +93,7 @@ class ChallengeEditScreen extends ConsumerWidget {
           spacing: 8,
           children: difficulties.map((level) {
             return ChoiceChip(
-              label: Text(level),
+              label: Text(level.toString()),
               selected: challenge.difficulty == level,
               onSelected: (selected) {
                 if (selected) {

+ 359 - 0
lib/features/challenge/views/mock_page.dart

@@ -0,0 +1,359 @@
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import 'package:japp_flutter/core/constants/options.dart';
+import 'package:japp_flutter/features/widgets/app_dropdown.dart';
+
+
+class MockPage extends StatelessWidget {
+  const MockPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Flutter 表单示例',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+      ),
+      home: const MyFormPage(),
+    );
+  }
+}
+
+// 表单数据模型
+class FormData {
+  String name;
+  String email;
+  String phone;
+  String password;
+  bool agreeTerms;
+  DateTime? startDate;
+  DateTime? endDate;
+  int? difficult;
+  FormData({
+    this.name = '',
+    this.email = 'fin@qq.com',
+    this.phone = '13770656091',
+    this.password = '',
+    this.agreeTerms = false,
+    this.startDate,
+    this.endDate,
+    this.difficult = 2,
+  });
+
+  // 转换为Map方便打印
+  Map<String, dynamic> toMap() {
+    return {
+      'name': name,
+      'email': email,
+      'phone': phone,
+      'password': password.replaceAll(RegExp(r'.'), '*'), // 密码用*号代替
+      'agreeTerms': agreeTerms,
+      'startDate': startDate != null ? DateFormat('yyyy-MM-dd').format(startDate!) : null,
+      'endDate': endDate != null ? DateFormat('yyyy-MM-dd').format(endDate!) : null,
+      'difficult':difficult
+    };
+  }
+}
+
+
+
+// 难度等级模型
+class DifficultyLevel {
+  final int value;
+  final String label;
+  final IconData? icon;
+
+  const DifficultyLevel({
+    required this.value,
+    required this.label,
+    this.icon,
+  });
+}
+
+// 预定义的难度等级
+const List<DifficultyLevel> difficultyLevels = [
+  DifficultyLevel(value: 1, label: '简单', icon: Icons.sentiment_very_satisfied),
+  DifficultyLevel(value: 2, label: '中等', icon: Icons.sentiment_satisfied),
+  DifficultyLevel(value: 3, label: '困难', icon: Icons.sentiment_dissatisfied),
+  DifficultyLevel(value: 4, label: '专家', icon: Icons.sentiment_very_dissatisfied),
+];
+
+
+class MyFormPage extends StatefulWidget {
+  const MyFormPage({super.key});
+
+  @override
+  State<MyFormPage> createState() => _MyFormPageState();
+}
+
+class _MyFormPageState extends State<MyFormPage> {
+  final _formKey = GlobalKey<FormState>();
+  final FormData _formData = FormData(
+      startDate: DateTime.now(), // 默认开始日期为今天
+      endDate: DateTime.now().add(const Duration(days: 7)), // 默认结束日期为7天后
+  );
+
+  // 选择开始日期
+  Future<void> _selectStartDate() async {
+    final DateTime? picked = await showDatePicker(
+      context: context,
+      initialDate: _formData.startDate ?? DateTime.now(),
+      firstDate: DateTime(2000),
+      lastDate: DateTime(2100),
+    );
+    if (picked != null && picked != _formData.startDate) {
+      setState(() {
+        _formData.startDate = picked;
+        // 如果结束日期早于开始日期,重置结束日期
+        if (_formData.endDate != null && _formData.endDate!.isBefore(picked)) {
+          _formData.endDate = null;
+        }
+      });
+    }
+  }
+
+  // 选择结束日期
+  Future<void> _selectEndDate() async {
+    final DateTime? picked = await showDatePicker(
+      context: context,
+      initialDate: _formData.endDate ?? (_formData.startDate ?? DateTime.now()),
+      firstDate: _formData.startDate ?? DateTime.now(),
+      lastDate: DateTime(2100),
+    );
+    if (picked != null && picked != _formData.endDate) {
+      setState(() {
+        _formData.endDate = picked;
+      });
+    }
+  }
+
+
+  // 提交表单
+  void _submitForm() {
+    if (_formKey.currentState!.validate()) {
+      _formKey.currentState!.save();
+      
+      // 获取完整的难度信息(不仅仅是value)
+      final selectedDifficulty = difficultyLevels.firstWhere(
+        (level) => level.value == _formData.difficult,
+        orElse: () => const DifficultyLevel(value: 0, label: '未知'),
+      );
+
+      print('表单提交数据: ${_formData.toMap()}');
+      
+      // 显示提交成功的提示
+      ScaffoldMessenger.of(context).showSnackBar(
+        const SnackBar(content: Text('表单提交成功! 数据已打印到控制台')),
+      );
+    }
+  }
+
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Flutter 表单示例'),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(16.0),
+        child: Form(
+          key: _formKey,
+          child: ListView(
+            children: [
+              TextFormField(
+                decoration: const InputDecoration(
+                  labelText: '姓名',
+                  hintText: '请输入您的姓名',
+                  border: OutlineInputBorder(),
+                ),
+                validator: (value) {
+                  if (value == null || value.isEmpty) {
+                    return '请输入姓名';
+                  }
+                  return null;
+                },
+                onSaved: (value) => _formData.name = value!,
+              ),
+              const SizedBox(height: 16),
+              TextFormField(
+                initialValue: _formData.email,
+                decoration: const InputDecoration(
+                  labelText: '电子邮件',
+                  hintText: '请输入您的电子邮件',
+                  border: OutlineInputBorder(),
+                ),
+                keyboardType: TextInputType.emailAddress,
+                validator: (value) {
+                  if (value == null || value.isEmpty) {
+                    return '请输入电子邮件';
+                  }
+                  if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
+                    return '请输入有效的电子邮件地址';
+                  }
+                  return null;
+                },
+                onSaved: (value) => _formData.email = value!,
+              ),
+              const SizedBox(height: 16),
+              TextFormField(
+                initialValue: _formData.phone,
+                decoration: const InputDecoration(
+                  labelText: '手机号码',
+                  hintText: '请输入您的手机号码',
+                  border: OutlineInputBorder(),
+                ),
+                keyboardType: TextInputType.phone,
+                validator: (value) {
+                  if (value == null || value.isEmpty) {
+                    return '请输入手机号码';
+                  }
+                  if (!RegExp(r'^[0-9]{11}$').hasMatch(value)) {
+                    return '请输入有效的11位手机号码';
+                  }
+                  return null;
+                },
+                onSaved: (value) => _formData.phone = value!,
+              ),
+              const SizedBox(height: 16),
+              TextFormField(
+                decoration: const InputDecoration(
+                  labelText: '密码',
+                  hintText: '请输入您的密码',
+                  border: OutlineInputBorder(),
+                ),
+                obscureText: true,
+                validator: (value) {
+                  if (value == null || value.isEmpty) {
+                    return '请输入密码';
+                  }
+                  if (value.length < 6) {
+                    return '密码长度不能少于6位';
+                  }
+                  return null;
+                },
+                onSaved: (value) => _formData.password = value!,
+              ),
+              const SizedBox(height: 16),
+              // 开始日期选择
+              InkWell(
+                onTap: _selectStartDate,
+                child: InputDecorator(
+                  decoration: const InputDecoration(
+                    labelText: '开始日期',
+                    border: OutlineInputBorder(),
+                  ),
+                  child: Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                    children: [
+                      Text(
+                        _formData.startDate != null
+                            ? DateFormat('yyyy-MM-dd').format(_formData.startDate!)
+                            : '请选择开始日期',
+                      ),
+                      const Icon(Icons.calendar_today, size: 20),
+                    ],
+                  ),
+                ),
+              ),
+              const SizedBox(height: 16),
+              // 结束日期选择
+              InkWell(
+                onTap: _formData.startDate == null
+                    ? null
+                    : _selectEndDate,
+                child: InputDecorator(
+                  decoration: const InputDecoration(
+                    labelText: '结束日期',
+                    border: OutlineInputBorder(),
+                  ),
+                  child: Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                    children: [
+                      Text(
+                        _formData.endDate != null
+                            ? DateFormat('yyyy-MM-dd').format(_formData.endDate!)
+                            : '请选择结束日期',
+                      ),
+                      const Icon(Icons.calendar_today, size: 20),
+                    ],
+                  ),
+                ),
+              ),
+              const SizedBox(height: 16),
+              // 难度选择下拉框
+              // DropdownButtonFormField<int>(
+              //   decoration: const InputDecoration(
+              //     labelText: '挑战难度',
+              //     border: OutlineInputBorder(),
+              //   ),
+              //   value: _formData.difficult,
+              //   items: difficultyLevels.map((level) {
+              //     return DropdownMenuItem<int>(
+              //       value: level.value,
+              //       child: Row(
+              //         children: [
+              //           if (level.icon != null) 
+              //             Icon(level.icon, size: 20),
+              //           const SizedBox(width: 8),
+              //           Text(level.label),
+              //         ],
+              //       ),
+              //     );
+              //   }).toList(),
+              //   onChanged: (value) {
+              //     setState(() {
+              //       _formData.difficult = value;
+              //     });
+              //   },
+              //   validator: (value) {
+              //     if (value == null) {
+              //       return '请选择难度等级';
+              //     }
+              //     return null;
+              //   },
+              //   onSaved: (value) => _formData.difficult = value,
+              // ),
+
+              AppDropdown(
+                label: '挑战难度', 
+                value: _formData.difficult, 
+                items: challengeDifficultyOpts, 
+                onChanged: (value) {
+                  setState(() {
+                    _formData.difficult = value;
+                  });
+                },
+              ),
+
+
+              const SizedBox(height: 16),
+
+              Row(
+                children: [
+                  Checkbox(
+                    value: _formData.agreeTerms,
+                    onChanged: (value) {
+                      setState(() {
+                        _formData.agreeTerms = value!;
+                      });
+                    },
+                  ),
+                  const Text('我同意用户协议'),
+                ],
+              ),
+              const SizedBox(height: 24),
+              ElevatedButton(
+                onPressed: _submitForm,
+                style: ElevatedButton.styleFrom(
+                  padding: const EdgeInsets.symmetric(vertical: 16),
+                ),
+                child: const Text('提交表单'),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 0 - 182
lib/features/challenge/views/widgets/challenge_card.dart

@@ -1,182 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:intl/intl.dart';
-import 'package:japp_flutter/core/models/challenge_model.dart';
-
-
-class ChallengeCard extends StatelessWidget {
-  final ChallengeModel challenge;
-
-  const ChallengeCard({super.key, required this.challenge});
-
-  @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,
-                    ),
-                  ),
-                ),
-                _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?? 0,
-            ),
-            const SizedBox(height: 12),
-            _CardFooter(
-              participants: challenge.participants,
-              remainingDays: challenge.remainingDays ?? 0,
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
-
-// 将复杂子组件拆分为独立组件
-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])),
-        ],
-      ),
-    );
-  }
-}

+ 37 - 0
lib/features/challenge/views/widgets/challenge_list_card_widget.dart

@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+import 'package:japp_flutter/core/models/challenge_model.dart';
+
+class ChallengeListCard extends StatelessWidget {
+  final ChallengeModel challenge;
+
+  const ChallengeListCard({super.key, required this.challenge});
+  @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: [
+            Text(
+              challenge.title,
+              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+            ),
+            const SizedBox(height: 16),
+
+            Text(
+              challenge.description,
+              style: TextStyle(color: Colors.grey[700], fontSize: 14),
+            ),
+            const SizedBox(height: 16),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 69 - 0
lib/features/widgets/app_dropdown.dart

@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:japp_flutter/core/models/dropdown_item.dart';
+
+class AppDropdown<T> extends StatelessWidget {
+  final String label;
+  final T? value;
+  final List<DropdownItem<T>> items; // 改为接收通用列表
+  final ValueChanged<T?> onChanged;
+  final FormFieldValidator<T>? validator;
+  final String? hintText;
+  final bool isExpanded;
+  final Widget? prefixIcon;
+  final InputBorder? border;
+  final TextStyle? textStyle;
+  final double? iconSize;
+  final Color? iconColor;
+
+  const AppDropdown({
+    super.key,
+    required this.label,
+    required this.value,
+    required this.items,
+    required this.onChanged,
+    this.validator,
+    this.hintText,
+    this.isExpanded = true,
+    this.prefixIcon,
+    this.border,
+    this.textStyle,
+    this.iconSize = 20.0,
+    this.iconColor,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return DropdownButtonFormField<T>(
+      decoration: InputDecoration(
+        labelText: label,
+        hintText: hintText,
+        border: border ?? const OutlineInputBorder(),
+        prefixIcon: prefixIcon,
+      ),
+      value: value,
+      items: items.map((item) {
+        return DropdownMenuItem<T>(
+          value: item.value,
+          child: Row(
+            children: [
+              if (item.icon != null)
+                Icon(
+                  item.icon,
+                  size: iconSize,
+                  color: item.iconColor ?? iconColor,
+                ),
+              if (item.icon != null) const SizedBox(width: 8),
+              Text(
+                item.label,
+                style: textStyle ?? Theme.of(context).textTheme.bodyMedium,
+              ),
+            ],
+          ),
+        );
+      }).toList(),
+      onChanged: onChanged,
+      validator: validator,
+      isExpanded: isExpanded,
+    );
+  }
+}

+ 1 - 1
lib/main.dart

@@ -16,7 +16,7 @@ class ChallengeApp extends StatelessWidget {
         primarySwatch: Colors.blue,
         appBarTheme: const AppBarTheme(elevation: 1),
       ),
-      initialRoute: AppRouter.challengeList,
+      initialRoute: AppRouter.mockPage,
       onGenerateRoute: AppRouter.generateRoute,
     );
   }

+ 0 - 30
test/widget_test.dart

@@ -1,30 +0,0 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:japp_flutter/main.dart';
-
-void main() {
-  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
-    // Build our app and trigger a frame.
-    await tester.pumpWidget(const MyApp());
-
-    // Verify that our counter starts at 0.
-    expect(find.text('0'), findsOneWidget);
-    expect(find.text('1'), findsNothing);
-
-    // Tap the '+' icon and trigger a frame.
-    await tester.tap(find.byIcon(Icons.add));
-    await tester.pump();
-
-    // Verify that our counter has incremented.
-    expect(find.text('0'), findsNothing);
-    expect(find.text('1'), findsOneWidget);
-  });
-}