Quellcode durchsuchen

完善下目标的添加表单screen

ValiZhang vor 9 Monaten
Ursprung
Commit
4c7c9d8221

+ 16 - 24
lib/core/constants/options.dart

@@ -1,30 +1,6 @@
 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 = [
@@ -33,3 +9,19 @@ const List<DropdownItem<int>> challengeDifficultyOpts = [
   DropdownItem(value: 3, label: '困难',icon:Icons.sentiment_dissatisfied),
   DropdownItem(value: 4, label: '地狱',icon: Icons.sentiment_very_dissatisfied),
 ];
+
+
+// 选项常量(放在 constants.dart 或表单文件顶部)
+const List<DropdownItem<int>> challengeActionTypeOpts = [
+  DropdownItem(value: 1, label: '愿景'),
+  DropdownItem(value: 2, label: '战略目标'),
+  DropdownItem(value: 3, label: '挑战目标'),
+  DropdownItem(value: 4, label: '待办目标'),
+];
+
+const List<DropdownItem<int>> challengeStatusOpts = [
+  DropdownItem(value: 1, label: '正常'),
+  DropdownItem(value: 2, label: '进行中'),
+  DropdownItem(value: 3, label: '已完成'),
+  DropdownItem(value: 4, label: '失败放弃'),
+];

+ 104 - 7
lib/core/models/challenge_model.dart

@@ -1,4 +1,5 @@
 import 'package:flutter/foundation.dart';
+import 'package:intl/intl.dart';
 
 // 待办
 @immutable
@@ -55,13 +56,11 @@ class ChallengeModel {
   }
 
   /// 转换为JSON
-  // Map<String, dynamic> toJson() => {
-  //   if (id != null) 'id': id,
-  //   'title': title,
-  //   'description': description,
-  //   'startDate': startDate.toIso8601String(),
-  //   'endDate': endDate.toIso8601String(),
-  // };
+  Map<String, dynamic> toJson() => {
+    if (id != null) 'id': id,
+    'title': title,
+    'description': description,
+  };
 
   ChallengeModel copyWith({
     int? id,
@@ -97,3 +96,101 @@ class ChallengeModel {
     );
   }
 }
+
+
+
+// 挑战类的 form 表单,用于绑定等
+class ChallengeFormModel {
+  int? id;
+  int actionType; // 1-愿景 2-战略目标 3-挑战目标 4-待办目标
+  String title;
+  String description;
+  String cover;
+  int sort;
+  int status; // 1-正常 2-进行中 3-已完成 4-失败放弃
+  int difficulty; // 1-简单 2-正常 3-困难 4-地狱
+  String remark;
+  DateTime? startDate;
+  DateTime? endDate;
+  DateTime? finishDate;
+  DateTime? planFinishDate;
+  int parentId;
+
+  ChallengeFormModel({
+    this.id,
+    this.actionType = 0,
+    this.title = '',
+    this.description = '',
+    this.cover = '',
+    this.sort = 100,
+    this.status = 0,
+    this.difficulty = 0,
+    this.remark = '',
+    this.startDate,
+    this.endDate,
+    this.finishDate,
+    this.planFinishDate,
+    this.parentId = 0
+  });
+
+  // 转换为Map方便打印
+  Map<String, dynamic> toMap() {
+    return {
+      'id': id,
+      'actionType': actionType ,
+      'title': title,
+      'description': description,
+      'cover': cover,
+      'sort': sort,
+      'status': status,
+      'difficulty': difficulty,
+      'remark': remark,
+      'startDate': startDate != null ? DateFormat('yyyy-MM-dd').format(startDate!) : null,
+      'endDate': endDate != null ? DateFormat('yyyy-MM-dd').format(endDate!) : null,
+      'finishDate': finishDate != null ? DateFormat('yyyy-MM-dd').format(finishDate!) : null,
+      'planFinishDate': planFinishDate != null ? DateFormat('yyyy-MM-dd').format(planFinishDate!) : null,
+      'parentId': parentId,
+    };
+  }
+
+  // 添加转换方法
+  factory ChallengeFormModel.fromChallenge(ChallengeModel challenge) {
+    return ChallengeFormModel(
+      id: challenge.id,
+      actionType: challenge.actionType,
+      title: challenge.title,
+      description: challenge.description,
+      cover: challenge.cover,
+      sort: challenge.sort,
+      status: challenge.status,
+      difficulty: challenge.difficulty,
+      remark: challenge.remark,
+      startDate: challenge.startDate,
+      endDate: challenge.endDate,
+      finishDate: challenge.finishDate,
+      planFinishDate: challenge.planFinishDate,
+      parentId: challenge.parentId,
+    );
+  }
+
+  ChallengeModel toChallenge() {
+    return ChallengeModel(
+      id: id,
+      actionType: actionType,
+      title: title,
+      description: description,
+      cover: cover,
+      sort: sort,
+      status: status,
+      difficulty: difficulty,
+      remark: remark,
+      startDate: startDate,
+      endDate: endDate,
+      finishDate: finishDate,
+      planFinishDate: planFinishDate,
+      parentId: parentId,
+    );
+  }
+
+
+}

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

@@ -1,23 +0,0 @@
-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;
-}

+ 43 - 3
lib/features/challenge/view_models/challenge_add_vm.dart

@@ -43,9 +43,9 @@ class AddChallengeViewModel extends AsyncNotifier<ChallengeModel> {
     state = AsyncData(state.value!.copyWith(description: description));
   }
 
-  /// 更新难度级别
-  void updateDifficulty(int difficulty) {
-    state = AsyncData(state.value!.copyWith(difficulty: difficulty));
+  /// 更新目标类型
+  void updateActionType(int actionType) {
+    state = AsyncData(state.value!.copyWith(actionType: actionType));
   }
 
   /// 更新日期范围
@@ -53,6 +53,46 @@ class AddChallengeViewModel extends AsyncNotifier<ChallengeModel> {
     state = AsyncData(state.value!.copyWith(startDate: start, endDate: end));
   }
 
+  /// 更新封面
+  void updateCover(String cover) {
+    state = AsyncData(state.value!.copyWith(cover: cover));
+  }
+
+  /// 更新排序
+  void updateSort(int sort) {
+    state = AsyncData(state.value!.copyWith(sort: sort));
+  }
+  
+  /// 更新挑战状态
+  void updateStatus(int status) {
+    state = AsyncData(state.value!.copyWith(status: status));
+  }
+
+  /// 更新难度级别
+  void updateDifficulty(int difficulty) {
+    state = AsyncData(state.value!.copyWith(difficulty: difficulty));
+  }
+
+  /// 更新备注
+  void updateRemark(String remark) {
+    state = AsyncData(state.value!.copyWith(remark: remark));
+  }
+
+  /// 更新所属父目标
+  void updateParentId(int parentId) {
+    state = AsyncData(state.value!.copyWith(parentId: parentId));
+  }
+
+  /// 更新计划完成时间
+  void updatePlanFinishDate(DateTime planFinishDate) {
+    state = AsyncData(state.value!.copyWith(planFinishDate: planFinishDate));
+  }
+
+  /// 更新完成时间
+  void updateFinishDate(DateTime finishDate) {
+    state = AsyncData(state.value!.copyWith(finishDate: finishDate));
+  }
+
   /// 提交新挑战
   Future<void> submitChallenge() async {
     if (state.value == null) return;

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

@@ -22,8 +22,12 @@ class ChallengeListViewModel extends AsyncNotifier<List<ChallengeModel>> {
   Future<List<ChallengeModel>> loadChallenges() async {
     state = const AsyncValue.loading();
     try {
+
+
       final challenges = await _repository.getChallenges();
       state = AsyncValue.data(challenges);
+
+      print('表单提交数据: ${challenges[0].toJson()}');
       return challenges;
     } catch (e, stack) {
       state = AsyncValue.error(e, stack);

+ 108 - 77
lib/features/challenge/views/add_challenge_screen.dart

@@ -5,6 +5,8 @@ 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';
+import 'package:japp_flutter/features/widgets/app_dropdown.dart';
+import 'package:japp_flutter/features/widgets/date_picker.dart';
 
 class AddChallengeScreen extends ConsumerWidget {
   const AddChallengeScreen({super.key});
@@ -49,88 +51,116 @@ class _BuildForm extends ConsumerWidget {
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
-          _TitleField(initialValue: challenge.title),
-          const SizedBox(height: 24),
+          TextFormField(
+            initialValue: challenge.title,
+            decoration: const InputDecoration(
+              labelText: '目标',
+              hintText: '请输入目标名称',
+              border: OutlineInputBorder(),
+            ),
+            onChanged: (value) =>
+                ref.read(addChallengeProvider.notifier).updateTitle(value),
+          ),
+
+          const SizedBox(height: 16),
+          TextFormField(
+            initialValue: challenge.description,
+            decoration: const InputDecoration(
+              labelText: '简介',
+              hintText: '请输入目标的简介',
+              border: OutlineInputBorder(),
+              alignLabelWithHint: true,
+            ),
+            maxLines: 3,
+            onChanged: (value) => ref
+                .read(addChallengeProvider.notifier)
+                .updateDescription(value),
+          ),
+
+          const SizedBox(height: 16),
+          AppDropdown(
+            label: '目标类型',
+            value: challenge.actionType,
+            items: challengeActionTypeOpts,
+            onChanged: (value) => ref
+                .read(addChallengeProvider.notifier)
+                .updateActionType(value ?? 0),
+          ),
+
+          const SizedBox(height: 16),
+          AppDropdown(
+            label: '挑战状态',
+            value: challenge.status,
+            items: challengeStatusOpts,
+            onChanged: (value) => ref
+                .read(addChallengeProvider.notifier)
+                .updateStatus(value ?? 0),
+          ),
+
+          const SizedBox(height: 16),
+          AppDropdown(
+            label: '挑战难度',
+            value: challenge.difficulty,
+            items: challengeDifficultyOpts,
+            onChanged: (value) => ref
+                .read(addChallengeProvider.notifier)
+                .updateDifficulty(value ?? 0),
+          ),
+
+          const SizedBox(height: 16),
           _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),
-    );
-  }
-}
+          const SizedBox(height: 16),
+          TextFormField(
+            keyboardType: TextInputType.number,
+            initialValue: challenge.sort.toString(),
+            decoration: const InputDecoration(
+              labelText: '排序',
+              border: OutlineInputBorder(),
+              alignLabelWithHint: true,
+            ),
+            onChanged: (value) => ref
+                .read(addChallengeProvider.notifier)
+                .updateSort(int.parse(value)),
+          ),
 
-class _DescriptionField extends ConsumerWidget {
-  final String initialValue;
 
-  const _DescriptionField({required this.initialValue});
+          const SizedBox(height: 16),
+          DatePickerField(
+            labelText: '计划完成日期',
+            onDateSelected: (date) => ref
+                .read(addChallengeProvider.notifier)
+                .updatePlanFinishDate(date!),
+          ),
 
-  @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),
-    );
-  }
-}
+          const SizedBox(height: 16),
+          DatePickerField(
+            labelText: '完成日期',
+            onDateSelected: (date) => ref
+                .read(addChallengeProvider.notifier)
+                .updateFinishDate(date!),
+          ),
 
-class _DifficultySelector extends ConsumerWidget {
-  final int currentDifficulty;
-  // static const difficulties = ['简单', '中等', '困难', '地狱'];
+          const SizedBox(height: 16),
+          TextFormField(
+            initialValue: challenge.remark,
+            decoration: const InputDecoration(
+              labelText: '备注',
+              border: OutlineInputBorder(),
+              alignLabelWithHint: true,
+            ),
+            maxLines: 3,
+            onChanged: (value) =>
+                ref.read(addChallengeProvider.notifier).updateRemark(value),
+          ),
+          const SizedBox(height: 16),
 
-  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: difficultOptions.map((option) {
-            return ChoiceChip(
-              label: Text(option.label),
-              selected: currentDifficulty == option.value,
-              onSelected: (selected) {
-                if (selected) {
-                  ref.read(addChallengeProvider.notifier).updateDifficulty(option.value);
-                }
-              },
-            );
-          }).toList(),
-        ),
-      ],
+        ],
+      ),
     );
   }
 }
@@ -139,12 +169,13 @@ class _DateRangeField extends ConsumerWidget {
   final DateTime startDate;
   final DateTime endDate;
 
-  const _DateRangeField({
-    required this.startDate,
-    required this.endDate,
-  });
+  const _DateRangeField({required this.startDate, required this.endDate});
 
-  Future<void> _selectDate(BuildContext context, bool isStartDate, WidgetRef ref) async {
+  Future<void> _selectDate(
+    BuildContext context,
+    bool isStartDate,
+    WidgetRef ref,
+  ) async {
     final initialDate = isStartDate ? startDate : endDate;
     final picked = await showDatePicker(
       context: context,
@@ -195,4 +226,4 @@ class _DateRangeField extends ConsumerWidget {
       ],
     );
   }
-}
+}

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

@@ -3,6 +3,11 @@ 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;
 

+ 154 - 0
lib/features/widgets/date_picker.dart

@@ -0,0 +1,154 @@
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+
+/// 通用日期选择器组件
+/// 支持功能:
+/// - 初始日期设置
+/// - 日期范围限制
+/// - 自定义日期格式
+/// - 主题适配
+/// - 空值处理
+/// - 验证支持
+class DatePickerField extends StatefulWidget {
+  final DateTime? initialDate;
+  final DateTime? firstDate;
+  final DateTime? lastDate;
+  final ValueChanged<DateTime?> onDateSelected;
+  final String labelText;
+  final String? hintText;
+  final bool isRequired;
+  final TextStyle? textStyle;
+  final InputDecoration? decoration;
+  final DateFormat? dateFormat;
+  final bool allowClear;
+  final bool autoValidate;
+
+  const DatePickerField({
+    super.key,
+    this.initialDate,
+    this.firstDate,
+    this.lastDate,
+    required this.onDateSelected,
+    this.labelText = '选择日期',
+    this.hintText,
+    this.isRequired = false,
+    this.textStyle,
+    this.decoration,
+    this.dateFormat,
+    this.allowClear = true,
+    this.autoValidate = false,
+  });
+
+  @override
+  State<DatePickerField> createState() => _DatePickerFieldState();
+}
+
+class _DatePickerFieldState extends State<DatePickerField> {
+  late final TextEditingController _controller;
+  DateTime? _selectedDate;
+  final DateFormat _defaultFormat = DateFormat('yyyy-MM-dd');
+
+  @override
+  void initState() {
+    super.initState();
+    _selectedDate = widget.initialDate;
+    _controller = TextEditingController(
+      text: _selectedDate != null ? _getFormattedDate(_selectedDate!) : '',
+    );
+  }
+
+  @override
+  void didUpdateWidget(covariant DatePickerField oldWidget) {
+    if (widget.initialDate != oldWidget.initialDate) {
+      _updateDate(widget.initialDate);
+    }
+    super.didUpdateWidget(oldWidget);
+  }
+
+  String _getFormattedDate(DateTime date) {
+    return (widget.dateFormat ?? _defaultFormat).format(date);
+  }
+
+  void _updateDate(DateTime? newDate) {
+    setState(() {
+      _selectedDate = newDate;
+      _controller.text = newDate != null ? _getFormattedDate(newDate) : '';
+    });
+    widget.onDateSelected(newDate);
+  }
+
+  Future<void> _selectDate() async {
+    final DateTime? picked = await showDatePicker(
+      context: context,
+      initialDate: _selectedDate ?? DateTime.now(),
+      firstDate: widget.firstDate ?? DateTime(1900),
+      lastDate: widget.lastDate ?? DateTime(2100),
+      builder: (context, child) {
+        return Theme(
+          data: Theme.of(context).copyWith(
+            colorScheme: ColorScheme.light(
+              primary: Theme.of(context).primaryColor,
+            ),
+          ),
+          child: child!,
+        );
+      },
+    );
+
+    if (picked != null && picked != _selectedDate) {
+      _updateDate(picked);
+    }
+  }
+
+  void _clearDate() {
+    _updateDate(null);
+  }
+
+  String? _validateDate(String? value) {
+    if (widget.isRequired && _selectedDate == null) {
+      return '请选择日期';
+    }
+    return null;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return TextFormField(
+      controller: _controller,
+      readOnly: true,
+      style: widget.textStyle ?? Theme.of(context).textTheme.bodyMedium,
+      decoration: (widget.decoration ?? InputDecoration(
+        labelText: widget.labelText,
+        hintText: widget.hintText,
+        suffixIcon: _buildSuffixIcon(),
+      )).copyWith(
+        errorStyle: const TextStyle(height: 0.7),
+      ),
+      onTap: _selectDate,
+      validator: widget.autoValidate ? _validateDate : null,
+    );
+  }
+
+  Widget _buildSuffixIcon() {
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        if (widget.allowClear && _selectedDate != null)
+          IconButton(
+            icon: const Icon(Icons.clear, size: 18),
+            onPressed: _clearDate,
+          ),
+        const Padding(
+          padding: EdgeInsets.only(right: 8),
+          child: Icon(Icons.calendar_today, size: 18),
+        ),
+      ],
+    );
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+}

+ 1 - 1
lib/main.dart

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