From 3ea162f33ecc1fd8610d35b289680a9a55af008a Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Thu, 20 Nov 2025 23:40:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0=E9=A2=9C=E8=89=B2?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/stats_page.dart | 4 +- lib/pages/todo_page.dart | 1 + lib/widgets/calendar_form.dart | 457 +++++++++++------------ lib/widgets/common.dart | 12 +- lib/widgets/flisp_form.dart | 405 ++++++++++---------- lib/widgets/flisp_widget.dart | 36 +- lib/widgets/todo_form.dart | 654 +++++++++++++++++---------------- lib/widgets/todo_widget.dart | 12 +- lib/widgets/year_selector.dart | 2 +- 9 files changed, 798 insertions(+), 785 deletions(-) diff --git a/lib/pages/stats_page.dart b/lib/pages/stats_page.dart index f8b1590..ef2a6ff 100644 --- a/lib/pages/stats_page.dart +++ b/lib/pages/stats_page.dart @@ -123,7 +123,7 @@ class StatsPageState extends State { ], ), const SizedBox(height: 5), - buildFlispStatsCard(allFlisps), + buildFlispStatsCard(context, allFlisps), ], ); } @@ -178,7 +178,7 @@ class StatsPageState extends State { ], ), const SizedBox(height: 5), - Expanded(child: buildTodoStatsCard(allTodos)), + Expanded(child: buildTodoStatsCard(context, allTodos)), ], ); } diff --git a/lib/pages/todo_page.dart b/lib/pages/todo_page.dart index 882540c..60079ae 100644 --- a/lib/pages/todo_page.dart +++ b/lib/pages/todo_page.dart @@ -53,6 +53,7 @@ class TodoPageState extends State { children: [ _buildTodoTabs(), _buildTodoSegment(), + SizedBox(height: 6), Expanded(child: _buildActiveTodoList(context)), ], ); diff --git a/lib/widgets/calendar_form.dart b/lib/widgets/calendar_form.dart index a1aeceb..7d09939 100644 --- a/lib/widgets/calendar_form.dart +++ b/lib/widgets/calendar_form.dart @@ -24,268 +24,275 @@ class CalendarForm extends StatefulWidget { } class _CalendarFormState extends State { + final int maxTitleCount = 10; + @override void initState() { super.initState(); } - @override - Widget build(BuildContext context) { - final int maxTitleCount = 10; - final provider = Provider.of(context); + Widget buildTitle() { final colors = Theme.of(context).colorScheme; - Widget buildTitle() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - widget.isEditing ? Icons.edit_note : Icons.add_task, + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + widget.isEditing ? Icons.edit_note : Icons.add_task, + color: colors.primary, + size: 24, + ), + SizedBox(width: 8), + Text( + widget.isEditing ? '编辑日程事项' : '添加日程事项', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, color: colors.primary, - size: 24, ), - SizedBox(width: 8), - Text( - widget.isEditing ? '编辑日程事项' : '添加日程事项', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: colors.primary, - ), - ), - ], - ); - } + ), + ], + ); + } - String? titleFieldValidator(value) { - if (value == null || value.isEmpty) { - return '请输入标题'; - } - if (value.length > maxTitleCount) { - return '标题不能超过$maxTitleCount个字符'; - } - return null; + String? titleFieldValidator(value) { + if (value == null || value.isEmpty) { + return '请输入标题'; } + if (value.length > maxTitleCount) { + return '标题不能超过$maxTitleCount个字符'; + } + return null; + } - FormBuilderTextField buildTitleField() { - return FormBuilderTextField( - name: 'title', - initialValue: provider.formItem.title, - onChanged: (value) { - setState(() { - provider.formItem.title = value ?? ''; - }); - }, - decoration: InputDecoration( - label: RichText( - text: TextSpan( - text: '内容', - style: TextStyle(color: Colors.grey.shade700, fontSize: 16), - children: const [ - TextSpan( - text: '*', - style: TextStyle( - color: Colors.red, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + FormBuilderTextField buildTitleField(CalendarProvider provider) { + final colors = Theme.of(context).colorScheme; + + return FormBuilderTextField( + name: 'title', + initialValue: provider.formItem.title, + onChanged: (value) { + setState(() { + provider.formItem.title = value ?? ''; + }); + }, + decoration: InputDecoration( + label: RichText( + text: TextSpan( + text: '内容', + style: TextStyle(color: Colors.grey.shade700, fontSize: 16), + children: const [ + TextSpan( + text: '*', + style: TextStyle( + color: Colors.red, + fontSize: 18, + fontWeight: FontWeight.bold, ), - ], - ), + ), + ], ), - hintText: '请输入日程标题...', - hintStyle: TextStyle(color: Colors.grey), - counterText: '', - suffixText: '${provider.formItem.title.length}/$maxTitleCount', - border: buildFormBoard(), - enabledBorder: buildFormEnabledBoard(), - focusedBorder: buildFormFocusedBoard(colors), - filled: true, - fillColor: colors.surface, - prefixIcon: Icon(Icons.title, color: Colors.blue), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), ), - maxLength: maxTitleCount, - validator: (value) => titleFieldValidator(value), - ); + hintText: '请输入日程标题...', + hintStyle: TextStyle(color: Colors.grey), + counterText: '', + suffixText: '${provider.formItem.title.length}/$maxTitleCount', + border: buildFormBoard(), + enabledBorder: buildFormEnabledBoard(), + focusedBorder: buildFormFocusedBoard(colors), + filled: true, + fillColor: colors.surface, + prefixIcon: Icon(Icons.title, color: Colors.blue), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + maxLength: maxTitleCount, + validator: (value) => titleFieldValidator(value), + ); + } + + String? startTimeFieldValidator(CalendarProvider provider, value) { + if (value == null) { + return '请选择开始时间'; + } + if (value.isBefore(DateTime.now())) { + return '不能选择过去的日期'; } - String? startTimeFieldValidator(value) { - if (value == null) { - return '请选择开始时间'; - } - if (value.isBefore(DateTime.now())) { - return '不能选择过去的日期'; - } - - final endTime = provider.formItem.endTime; - if (endTime != null && endTime.isBefore(value)) { - return '结束时间不能早于开始时间'; - } - if (endTime != null && endTime.isAtSameMomentAs(value)) { - return '开始时间不能等于结束时间'; - } - - return null; + final endTime = provider.formItem.endTime; + if (endTime != null && endTime.isBefore(value)) { + return '结束时间不能早于开始时间'; + } + if (endTime != null && endTime.isAtSameMomentAs(value)) { + return '开始时间不能等于结束时间'; } - FormBuilderDateTimePicker buildStartTimeField() { - var startTime = provider.formItem.startTime; + return null; + } - return FormBuilderDateTimePicker( - name: 'startTime', - format: DateFormat('yyyy-MM-dd HH:mm:ss'), - initialValue: startTime, - inputType: InputType.both, - onChanged: (value) { - setState(() { - provider.formItem.startTime = value!; - }); - }, - decoration: InputDecoration( - labelText: - startTime == null ? '选择开始时间' : '开始时间: ${formatTime(startTime)}', - labelStyle: TextStyle( - color: startTime == null ? Colors.grey : Colors.black87, - ), - prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), - border: buildFormBoard(), - enabledBorder: buildFormEnabledBoard(), - focusedBorder: buildFormFocusedBoard(colors), - filled: true, - fillColor: colors.surface, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + FormBuilderDateTimePicker buildStartTimeField(CalendarProvider provider) { + final colors = Theme.of(context).colorScheme; + var startTime = provider.formItem.startTime; + + return FormBuilderDateTimePicker( + name: 'startTime', + format: DateFormat('yyyy-MM-dd HH:mm:ss'), + initialValue: startTime, + inputType: InputType.both, + onChanged: (value) { + setState(() { + provider.formItem.startTime = value!; + }); + }, + decoration: InputDecoration( + labelText: + startTime == null ? '选择开始时间' : '开始时间: ${formatTime(startTime)}', + labelStyle: TextStyle( + color: startTime == null ? Colors.grey : Colors.black87, ), - validator: (value) => startTimeFieldValidator(value), - ); + prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), + border: buildFormBoard(), + enabledBorder: buildFormEnabledBoard(), + focusedBorder: buildFormFocusedBoard(colors), + filled: true, + fillColor: colors.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + validator: (value) => startTimeFieldValidator(provider, value), + ); + } + + String? endTimeFieldValidator(CalendarProvider provider, value) { + if (value == null) { + return '请选择结束时间'; + } + if (value.isBefore(DateTime.now())) { + return '不能选择过去的日期'; } - String? endTimeFieldValidator(value) { - if (value == null) { - return '请选择结束时间'; - } - if (value.isBefore(DateTime.now())) { - return '不能选择过去的日期'; - } - - final startTime = provider.formItem.startTime; - if (startTime != null && value.isBefore(startTime)) { - return '结束时间不能早于开始时间'; - } - if (startTime != null && startTime.isAtSameMomentAs(value)) { - return '开始时间不能等于结束时间'; - } - - return null; + final startTime = provider.formItem.startTime; + if (startTime != null && value.isBefore(startTime)) { + return '结束时间不能早于开始时间'; + } + if (startTime != null && startTime.isAtSameMomentAs(value)) { + return '开始时间不能等于结束时间'; } - FormBuilderDateTimePicker buildEndTimeField() { - var endTime = provider.formItem.endTime; + return null; + } - return FormBuilderDateTimePicker( - name: 'endTime', - format: DateFormat('yyyy-MM-dd HH:mm:ss'), - initialValue: endTime, - inputType: InputType.both, - onChanged: (value) { - setState(() { - provider.formItem.endTime = value!; - }); - }, - decoration: InputDecoration( - labelText: - endTime == null ? '选择结束时间' : '结束时间: ${formatTime(endTime)}', - labelStyle: TextStyle( - color: endTime == null ? Colors.grey : Colors.black87, - ), - prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), - border: buildFormBoard(), - enabledBorder: buildFormEnabledBoard(), - focusedBorder: buildFormFocusedBoard(colors), - filled: true, - fillColor: colors.surface, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + FormBuilderDateTimePicker buildEndTimeField(CalendarProvider provider) { + final colors = Theme.of(context).colorScheme; + var endTime = provider.formItem.endTime; + + return FormBuilderDateTimePicker( + name: 'endTime', + format: DateFormat('yyyy-MM-dd HH:mm:ss'), + initialValue: endTime, + inputType: InputType.both, + onChanged: (value) { + setState(() { + provider.formItem.endTime = value!; + }); + }, + decoration: InputDecoration( + labelText: endTime == null ? '选择结束时间' : '结束时间: ${formatTime(endTime)}', + labelStyle: TextStyle( + color: endTime == null ? Colors.grey : Colors.black87, ), - validator: (value) => endTimeFieldValidator(value), - ); + prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), + border: buildFormBoard(), + enabledBorder: buildFormEnabledBoard(), + focusedBorder: buildFormFocusedBoard(colors), + filled: true, + fillColor: colors.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + validator: (value) => endTimeFieldValidator(provider, value), + ); + } + + IconButton buildClearScheduled(CalendarProvider provider) { + return IconButton( + icon: Icon(Icons.clear, size: 18), + onPressed: () { + setState(() { + provider.formItem.scheduledTime = null; + }); + widget.formKey.currentState?.fields['scheduledTime']?.didChange(null); + }, + ); + } + + String? scheduledTimeFieldValidator(value) { + if (value != null && value.isBefore(DateTime.now())) { + return '不能选择过去的日期'; } + return null; + } - IconButton buildClearScheduled() { - return IconButton( - icon: Icon(Icons.clear, size: 18), - onPressed: () { - setState(() { - provider.formItem.scheduledTime = null; - }); - widget.formKey.currentState?.fields['scheduledTime']?.didChange(null); - }, - ); - } + FormBuilderDateTimePicker buildScheduledTimeField(CalendarProvider provider) { + final colors = Theme.of(context).colorScheme; + var scheduledTime = provider.formItem.scheduledTime; - String? scheduledTimeFieldValidator(value) { - if (value != null && value.isBefore(DateTime.now())) { - return '不能选择过去的日期'; - } - return null; - } - - FormBuilderDateTimePicker buildScheduledTimeField() { - var scheduledTime = provider.formItem.scheduledTime; - - return FormBuilderDateTimePicker( - name: 'scheduledTime', - format: DateFormat('yyyy-MM-dd HH:mm:ss'), - initialValue: scheduledTime, - inputType: InputType.both, - onChanged: (value) { - setState(() { - provider.formItem.scheduledTime = value; - }); - }, - decoration: InputDecoration( - labelText: - scheduledTime == null - ? '选择提醒日期' - : '提醒: ${formatTime(scheduledTime)}', - labelStyle: TextStyle( - color: scheduledTime == null ? Colors.grey : Colors.black87, - ), - prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), - border: buildFormBoard(), - enabledBorder: buildFormEnabledBoard(), - focusedBorder: buildFormFocusedBoard(colors), - filled: true, - fillColor: colors.surface, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - suffixIcon: scheduledTime != null ? buildClearScheduled() : null, + return FormBuilderDateTimePicker( + name: 'scheduledTime', + format: DateFormat('yyyy-MM-dd HH:mm:ss'), + initialValue: scheduledTime, + inputType: InputType.both, + onChanged: (value) { + setState(() { + provider.formItem.scheduledTime = value; + }); + }, + decoration: InputDecoration( + labelText: + scheduledTime == null + ? '选择提醒日期' + : '提醒: ${formatTime(scheduledTime)}', + labelStyle: TextStyle( + color: scheduledTime == null ? Colors.grey : Colors.black87, ), - validator: (value) => scheduledTimeFieldValidator(value), - ); - } + prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), + border: buildFormBoard(), + enabledBorder: buildFormEnabledBoard(), + focusedBorder: buildFormFocusedBoard(colors), + filled: true, + fillColor: colors.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + suffixIcon: + scheduledTime != null ? buildClearScheduled(provider) : null, + ), + validator: (value) => scheduledTimeFieldValidator(value), + ); + } - FormBuilder buildForm() { - return FormBuilder( - key: widget.formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - buildTitleField(), - SizedBox(height: 12), - buildStartTimeField(), - SizedBox(height: 12), - buildEndTimeField(), - SizedBox(height: 12), - buildScheduledTimeField(), - ], - ), - ); - } + FormBuilder buildForm(CalendarProvider provider) { + return FormBuilder( + key: widget.formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + buildTitleField(provider), + SizedBox(height: 12), + buildStartTimeField(provider), + SizedBox(height: 12), + buildEndTimeField(provider), + SizedBox(height: 12), + buildScheduledTimeField(provider), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); return Padding( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10), child: Column( mainAxisSize: MainAxisSize.min, - children: [buildTitle(), SizedBox(height: 20), buildForm()], + children: [buildTitle(), SizedBox(height: 20), buildForm(provider)], ), ); } diff --git a/lib/widgets/common.dart b/lib/widgets/common.dart index 3df4663..03fbc51 100644 --- a/lib/widgets/common.dart +++ b/lib/widgets/common.dart @@ -193,16 +193,18 @@ Widget buildToggleSwitch({ required List labels, required ValueChanged onTabChanged, }) { + final colors = Theme.of(context).colorScheme; + return ToggleSwitch( minWidth: 90.0, minHeight: 40.0, initialLabelIndex: tabValues.indexOf(currentTab), totalSwitches: tabValues.length, labels: labels, - activeBgColor: [Theme.of(context).colorScheme.primary], + activeBgColor: [colors.primary], activeFgColor: Colors.white, - inactiveBgColor: Colors.grey.shade200, - inactiveFgColor: Colors.grey.shade700, + inactiveBgColor: colors.surfaceContainer, + inactiveFgColor: colors.onSurface, cornerRadius: 12.0, customTextStyles: [TextStyle(fontSize: 12, fontWeight: FontWeight.w500)], onToggle: (index) { @@ -216,7 +218,7 @@ Widget buildToggleSwitch({ ButtonStyle buildSegmentStyle(ColorScheme colors) { return ButtonStyle( side: WidgetStateProperty.all( - const BorderSide(color: Colors.transparent, width: 0), + BorderSide(color: colors.surface, width: 0), ), iconColor: WidgetStateProperty.resolveWith( (Set states) { @@ -232,7 +234,7 @@ ButtonStyle buildSegmentStyle(ColorScheme colors) { if (states.contains(WidgetState.selected)) { return colors.primary; } - return Colors.grey.shade200; + return colors.surfaceContainer; }), foregroundColor: WidgetStateProperty.resolveWith(( Set states, diff --git a/lib/widgets/flisp_form.dart b/lib/widgets/flisp_form.dart index 8319c87..37625f5 100644 --- a/lib/widgets/flisp_form.dart +++ b/lib/widgets/flisp_form.dart @@ -27,6 +27,7 @@ class FlispForm extends StatefulWidget { class _FlispFormState extends State { bool _showTagOptions = false; + final int maxContentCount = 100; @override void initState() { @@ -62,223 +63,211 @@ class _FlispFormState extends State { }); } - @override - Widget build(BuildContext context) { - final int maxContentCount = 100; - final provider = Provider.of(context); + Widget buildContentField(FlispProvider provider) { final colors = Theme.of(context).colorScheme; - Widget buildContentField() { - return FormBuilderTextField( - name: 'content', - initialValue: provider.formItem.content, - onChanged: (value) { - setState(() { - provider.formItem.content = value ?? ''; - }); - }, - decoration: InputDecoration( - hintText: '记录你的灵感瞬间...', - hintStyle: TextStyle(color: Colors.grey), - counterText: '', - suffixText: '${provider.formItem.content.length}/$maxContentCount', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: colors.primary), - ), - filled: true, - fillColor: colors.surface, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + return FormBuilderTextField( + name: 'content', + initialValue: provider.formItem.content, + onChanged: (value) { + setState(() { + provider.formItem.content = value ?? ''; + }); + }, + decoration: InputDecoration( + hintText: '记录你的灵感瞬间...', + hintStyle: TextStyle(color: Colors.grey), + counterText: '', + suffixText: '${provider.formItem.content.length}/$maxContentCount', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), ), - minLines: 4, - maxLines: 8, - maxLength: maxContentCount, - validator: (value) { - if (value != null && value.isEmpty) { - return '请输入内容'; - } - - if (value != null && value.length > maxContentCount) { - return '内容不能超过$maxContentCount个字符'; - } - return null; - }, - ); - } - - Widget buildFormBody() { - return Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - buildContentField(), - SizedBox(height: 8), - buildFlispTag(provider.formItem), - SizedBox(height: 12), - if (provider.formItem.imageUrl.isNotEmpty) - buildFlispImage( - flisp: provider.formItem, - showDelete: true, - onDelete: _deleteImage, - ), - ], - ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), ), - ); - } - - void selectImage() async { - FilePickerResult? result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['jpg', 'jpeg', 'png'], - allowMultiple: false, - ); - - if (result != null) { - PlatformFile file = result.files.first; - final fileName = await MinIOHelper().uploadFile(file: file); - _selectImage(fileName); - } - } - - Widget buildSelectImageButton() { - return GestureDetector( - onTap: () => selectImage(), - child: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: Colors.blue.withAlpha(50), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.withAlpha(80), width: 1), - ), - child: Icon( - Icons.photo_library_rounded, - size: 20, - color: Colors.blue, - ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colors.primary), ), - ); - } + filled: true, + fillColor: colors.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + minLines: 4, + maxLines: 8, + maxLength: maxContentCount, + validator: (value) { + if (value != null && value.isEmpty) { + return '请输入内容'; + } - Widget buildSelectTagButton() { - return GestureDetector( - onTap: _toggleTagOptions, - child: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: Colors.orange.withAlpha(50), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.orange.withAlpha(80), width: 1), - ), - child: Icon( - Icons.local_offer_rounded, - size: 20, - color: Colors.orange, - ), - ), - ); - } + if (value != null && value.length > maxContentCount) { + return '内容不能超过$maxContentCount个字符'; + } + return null; + }, + ); + } - Widget buildConfirmButton() { - return Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: colors.primary, - borderRadius: BorderRadius.circular(24), - ), - child: IconButton( - icon: Icon(Icons.arrow_forward_rounded, size: 24), - color: Colors.white, - onPressed: () => widget.onOk(), - ), - ); - } - - Widget buildShowTagOptions() { - // 计算弹窗位置(基于标签按钮位置) - return Positioned( - bottom: 50, // 调整垂直位置 - left: 60, // 调整水平位置 - child: Container( - width: 90, - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black26, - blurRadius: 8, - offset: Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: - FlispTag.values.map((tag) { - return GestureDetector( - onTap: () => _selectTag(tag), - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8), - child: Row( - children: [ - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: tag.color, - shape: BoxShape.circle, - ), - child: Icon( - tag.icon, - size: 12, - color: Colors.white, - ), - ), - SizedBox(width: 8), - Text(tag.label, style: TextStyle(fontSize: 12)), - ], - ), - ), - ); - }).toList(), - ), - ), - ); - } - - Widget buildBottomButton() { - return Container( - padding: EdgeInsets.only(top: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Widget buildFormBody(FlispProvider provider) { + return Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - buildSelectImageButton(), - SizedBox(width: 12), - buildSelectTagButton(), - ], - ), - buildConfirmButton(), + buildContentField(provider), + SizedBox(height: 8), + buildFlispTag(context, provider.formItem), + SizedBox(height: 12), + if (provider.formItem.imageUrl.isNotEmpty) + buildFlispImage( + flisp: provider.formItem, + showDelete: true, + onDelete: _deleteImage, + ), ], ), - ); + ), + ); + } + + void selectImage() async { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['jpg', 'jpeg', 'png'], + allowMultiple: false, + ); + + if (result != null) { + PlatformFile file = result.files.first; + final fileName = await MinIOHelper().uploadFile(file: file); + _selectImage(fileName); } + } + + Widget buildSelectImageButton() { + return GestureDetector( + onTap: () => selectImage(), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.blue.withAlpha(50), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.withAlpha(80), width: 1), + ), + child: Icon(Icons.photo_library_rounded, size: 20, color: Colors.blue), + ), + ); + } + + Widget buildSelectTagButton() { + return GestureDetector( + onTap: _toggleTagOptions, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.orange.withAlpha(50), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.withAlpha(80), width: 1), + ), + child: Icon(Icons.local_offer_rounded, size: 20, color: Colors.orange), + ), + ); + } + + Widget buildConfirmButton() { + return Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(24), + ), + child: IconButton( + icon: Icon(Icons.arrow_forward_rounded, size: 24), + color: Colors.white, + onPressed: () => widget.onOk(), + ), + ); + } + + Widget buildShowTagOptions() { + // 计算弹窗位置(基于标签按钮位置) + return Positioned( + bottom: 50, // 调整垂直位置 + left: 60, // 调整水平位置 + child: Container( + width: 90, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: + FlispTag.values.map((tag) { + return GestureDetector( + onTap: () => _selectTag(tag), + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8), + child: Row( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: tag.color, + shape: BoxShape.circle, + ), + child: Icon(tag.icon, size: 12, color: Colors.white), + ), + SizedBox(width: 8), + Text(tag.label, style: TextStyle(fontSize: 12)), + ], + ), + ), + ); + }).toList(), + ), + ), + ); + } + + Widget buildBottomButton() { + return Container( + padding: EdgeInsets.only(top: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + buildSelectImageButton(), + SizedBox(width: 12), + buildSelectTagButton(), + ], + ), + buildConfirmButton(), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); // 整体使用Stack布局分离事件层级 return Stack( @@ -300,7 +289,9 @@ class _FlispFormState extends State { // 主表单内容 FormBuilder( key: widget.formKey, - child: Column(children: [buildFormBody(), buildBottomButton()]), + child: Column( + children: [buildFormBody(provider), buildBottomButton()], + ), ), // 标签弹窗(放在最上层,确保事件优先响应) diff --git a/lib/widgets/flisp_widget.dart b/lib/widgets/flisp_widget.dart index 4263328..3795afc 100644 --- a/lib/widgets/flisp_widget.dart +++ b/lib/widgets/flisp_widget.dart @@ -3,7 +3,7 @@ import 'package:flisp_app/widgets/common.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -Widget buildFlispStatsCard(List flisps) { +Widget buildFlispStatsCard(BuildContext context, List flisps) { int totalCount = flisps.length; int lifeCount = flisps.where((flisp) => flisp.tag == FlispTag.life).length; @@ -15,16 +15,16 @@ Widget buildFlispStatsCard(List flisps) { mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildStatItem('总计', totalCount, Colors.orange), - _buildStatItem('生活', lifeCount, FlispTag.life.color), - _buildStatItem('工作', workCount, FlispTag.work.color), - _buildStatItem('学习', studyCount, FlispTag.study.color), + _buildStatItem(context, '总计', totalCount), + _buildStatItem(context, '生活', lifeCount), + _buildStatItem(context, '工作', workCount), + _buildStatItem(context, '学习', studyCount), ], ), ); } -Widget _buildStatItem(String label, int count, Color color) { +Widget _buildStatItem(BuildContext context, String label, int count) { return Column( children: [ Text( @@ -32,7 +32,7 @@ Widget _buildStatItem(String label, int count, Color color) { style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: color, + color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 4), @@ -209,13 +209,15 @@ Widget _buildFlispVideo(Flisp flisp) { ); } -Widget buildFlispTag(Flisp flisp) { +Widget buildFlispTag(BuildContext context, Flisp flisp) { + final colors = Theme.of(context).colorScheme; + return Container( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: flisp.tag.color.withAlpha(30), + color: colors.secondaryContainer, borderRadius: BorderRadius.circular(8), - border: Border.all(color: flisp.tag.color.withAlpha(60)), + // border: Border.all(color: flisp.tag.color.withAlpha(60)), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -224,7 +226,7 @@ Widget buildFlispTag(Flisp flisp) { width: 14, height: 14, decoration: BoxDecoration( - color: flisp.tag.color, + color: colors.secondary, shape: BoxShape.circle, ), child: Icon(flisp.tag.icon, size: 8, color: Colors.white), @@ -234,7 +236,7 @@ Widget buildFlispTag(Flisp flisp) { flisp.tag.label, style: TextStyle( fontSize: 12, - color: flisp.tag.color, + color: colors.secondary, fontWeight: FontWeight.w500, ), ), @@ -250,9 +252,13 @@ Widget buildFlispTime(Flisp flisp) { ); } -Widget _buildFlispSuffix(Flisp flisp) { +Widget _buildFlispSuffix(BuildContext context, Flisp flisp) { return Row( - children: [buildFlispTag(flisp), const Spacer(), buildFlispTime(flisp)], + children: [ + buildFlispTag(context, flisp), + const Spacer(), + buildFlispTime(flisp), + ], ); } @@ -281,7 +287,7 @@ Widget _buildFlispItem({ const SizedBox(height: 4), if (hasVideo) _buildFlispVideo(flisp), const SizedBox(height: 4), - _buildFlispSuffix(flisp), + _buildFlispSuffix(context, flisp), ], ), ), diff --git a/lib/widgets/todo_form.dart b/lib/widgets/todo_form.dart index 05b671a..6f319ed 100644 --- a/lib/widgets/todo_form.dart +++ b/lib/widgets/todo_form.dart @@ -22,350 +22,356 @@ class TodoForm extends StatefulWidget { } class _TodoFormState extends State { + final int maxTitleCount = 10; + final int maxContentCount = 20; + @override void initState() { super.initState(); } - @override - Widget build(BuildContext context) { - final int maxTitleCount = 10; - final int maxContentCount = 20; - final provider = Provider.of(context); + Widget buildTitle() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + widget.isEditing ? Icons.edit_note : Icons.add_task, + color: Theme.of(context).colorScheme.primary, + size: 24, + ), + SizedBox(width: 8), + Text( + widget.isEditing ? '编辑待办事项' : '添加待办事项', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ); + } + + FormBuilderTextField buildTitleField(TodoProvider provider) { final colors = Theme.of(context).colorScheme; - Widget buildTitle() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - widget.isEditing ? Icons.edit_note : Icons.add_task, - color: colors.primary, - size: 24, - ), - SizedBox(width: 8), - Text( - widget.isEditing ? '编辑待办事项' : '添加待办事项', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: colors.primary, - ), - ), - ], - ); - } - - FormBuilderTextField buildTitleField() { - return FormBuilderTextField( - name: 'title', - initialValue: provider.formItem.title, - onChanged: (value) { - setState(() { - provider.formItem.title = value ?? ''; - }); - }, - decoration: InputDecoration( - label: RichText( - text: TextSpan( - text: '标题', - style: TextStyle(color: Colors.grey.shade700, fontSize: 16), - children: const [ - TextSpan( - text: '*', - style: TextStyle( - color: Colors.red, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + return FormBuilderTextField( + name: 'title', + initialValue: provider.formItem.title, + onChanged: (value) { + setState(() { + provider.formItem.title = value ?? ''; + }); + }, + decoration: InputDecoration( + label: RichText( + text: TextSpan( + text: '标题', + style: TextStyle(color: Colors.grey.shade700, fontSize: 16), + children: const [ + TextSpan( + text: '*', + style: TextStyle( + color: Colors.red, + fontSize: 18, + fontWeight: FontWeight.bold, ), - ], - ), + ), + ], ), - hintText: '请输入待办事项标题...', - hintStyle: TextStyle(color: Colors.grey), - counterText: '', - suffixText: '${provider.formItem.title.length}/$maxTitleCount', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: colors.primary), - ), - filled: true, - fillColor: colors.surface, - prefixIcon: Icon(Icons.title, color: Colors.blue), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), ), - maxLength: maxTitleCount, - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入标题'; - } - if (value.length > maxTitleCount) { - return '标题不能超过$maxTitleCount个字符'; - } - return null; - }, - ); - } - - FormBuilderTextField buildContentField() { - return FormBuilderTextField( - name: 'content', - initialValue: provider.formItem.content, - onChanged: (value) { - setState(() { - provider.formItem.content = value ?? ''; - }); - }, - decoration: InputDecoration( - labelText: '内容', - hintText: '请输入待办事项内容...', - hintStyle: TextStyle(color: Colors.grey), - counterText: '', - suffixText: '${provider.formItem.content.length}/$maxContentCount', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: colors.primary), - ), - filled: true, - fillColor: colors.surface, - prefixIcon: Icon(Icons.description, color: Colors.green), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - maxLines: 2, - maxLength: maxContentCount, - validator: (value) { - if (value != null && value.length > maxContentCount) { - return '内容不能超过$maxContentCount个字符'; - } - return null; - }, - ); - } - - IconButton buildClearDueDateSuffixIcon() { - return IconButton( - icon: Icon(Icons.clear, size: 18), - onPressed: () { - setState(() { - provider.formItem.dueDate = null; - }); - widget.formKey.currentState?.fields['dueDate']?.didChange(null); - }, - ); - } - - FormBuilderDateTimePicker buildDueDateField() { - return FormBuilderDateTimePicker( - name: 'dueDate', - format: DateFormat('yyyy-MM-dd'), - initialValue: provider.formItem.dueDate, - inputType: InputType.date, - onChanged: (value) { - setState(() { - provider.formItem.dueDate = value; - }); - }, - decoration: InputDecoration( - labelText: - provider.formItem.dueDate == null - ? '选择截止日期' - : '截止: ${DateFormat('yyyy-MM-dd').format(provider.formItem.dueDate!)}', - labelStyle: TextStyle( - color: - provider.formItem.dueDate == null - ? Colors.grey - : Colors.black87, - ), - prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: colors.primary), - ), - filled: true, - fillColor: colors.surface, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - suffixIcon: - provider.formItem.dueDate != null - ? buildClearDueDateSuffixIcon() - : null, - ), - validator: (value) { - if (value != null && - value.isBefore(DateTime.now().subtract(Duration(days: 1)))) { - return '不能选择过去的日期'; - } - return null; - }, - ); - } - - IconButton buildClearScheduledTimeSuffixIcon() { - return IconButton( - icon: Icon(Icons.clear, size: 18), - onPressed: () { - setState(() { - provider.formItem.scheduledTime = null; - }); - widget.formKey.currentState?.fields['scheduledTime']?.didChange(null); - }, - ); - } - - FormBuilderDateTimePicker buildScheduledTimeField() { - return FormBuilderDateTimePicker( - name: 'scheduledTime', - format: DateFormat('yyyy-MM-dd HH:mm:ss'), - initialValue: provider.formItem.scheduledTime, - inputType: InputType.both, - onChanged: (value) { - setState(() { - provider.formItem.scheduledTime = value; - }); - }, - decoration: InputDecoration( - labelText: - provider.formItem.scheduledTime == null - ? '选择提醒日期' - : '提醒: ${DateFormat('yyyy-MM-dd HH:mm:ss').format(provider.formItem.scheduledTime!)}', - labelStyle: TextStyle( - color: - provider.formItem.scheduledTime == null - ? Colors.grey - : Colors.black87, - ), - prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: colors.primary), - ), - filled: true, - fillColor: colors.surface, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - suffixIcon: - provider.formItem.scheduledTime != null - ? buildClearScheduledTimeSuffixIcon() - : null, - ), - validator: (value) { - if (value != null && value.isBefore(DateTime.now())) { - return '不能选择过去的日期'; - } - return null; - }, - ); - } - - FormBuilderRadioGroup builderRadioGroup() { - return FormBuilderRadioGroup( - name: 'priority', - initialValue: provider.formItem.priority, - decoration: InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.zero, - ), - orientation: OptionsOrientation.horizontal, - wrapSpacing: 6, - options: - TodoPriority.values.map((priority) { - return FormBuilderFieldOption( - value: priority, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [Text(priority.label)], - ), - ); - }).toList(), - onChanged: (value) { - if (value != null) { - setState(() { - provider.formItem.priority = value; - }); - } - }, - ); - } - - Container buildPriorityField() { - return Container( - decoration: BoxDecoration( - color: colors.surface, + hintText: '请输入待办事项标题...', + hintStyle: TextStyle(color: Colors.grey), + counterText: '', + suffixText: '${provider.formItem.title.length}/$maxTitleCount', + border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), ), - padding: EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.flag, color: Colors.orange), - SizedBox(width: 6), - Text('优先级'), - ], - ), - builderRadioGroup(), - ], + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), ), - ); - } + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colors.primary), + ), + filled: true, + fillColor: colors.surface, + prefixIcon: Icon(Icons.title, color: Colors.blue), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + maxLength: maxTitleCount, + validator: (value) { + if (value == null || value.isEmpty) { + return '请输入标题'; + } + if (value.length > maxTitleCount) { + return '标题不能超过$maxTitleCount个字符'; + } + return null; + }, + ); + } - FormBuilder buildForm() { - return FormBuilder( - key: widget.formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - buildTitleField(), - SizedBox(height: 12), - buildContentField(), - SizedBox(height: 12), - buildDueDateField(), - SizedBox(height: 12), - buildScheduledTimeField(), - SizedBox(height: 12), - buildPriorityField(), - ], + FormBuilderTextField buildContentField(TodoProvider provider) { + final colors = Theme.of(context).colorScheme; + + return FormBuilderTextField( + name: 'content', + initialValue: provider.formItem.content, + onChanged: (value) { + setState(() { + provider.formItem.content = value ?? ''; + }); + }, + decoration: InputDecoration( + labelText: '内容', + hintText: '请输入待办事项内容...', + hintStyle: TextStyle(color: Colors.grey), + counterText: '', + suffixText: '${provider.formItem.content.length}/$maxContentCount', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), ), - ); - } + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colors.primary), + ), + filled: true, + fillColor: colors.surface, + prefixIcon: Icon(Icons.description, color: Colors.green), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + maxLines: 2, + maxLength: maxContentCount, + validator: (value) { + if (value != null && value.length > maxContentCount) { + return '内容不能超过$maxContentCount个字符'; + } + return null; + }, + ); + } + + IconButton buildClearDueDateSuffixIcon(TodoProvider provider) { + return IconButton( + icon: Icon(Icons.clear, size: 18), + onPressed: () { + setState(() { + provider.formItem.dueDate = null; + }); + widget.formKey.currentState?.fields['dueDate']?.didChange(null); + }, + ); + } + + FormBuilderDateTimePicker buildDueDateField(TodoProvider provider) { + final colors = Theme.of(context).colorScheme; + + return FormBuilderDateTimePicker( + name: 'dueDate', + format: DateFormat('yyyy-MM-dd'), + initialValue: provider.formItem.dueDate, + inputType: InputType.date, + onChanged: (value) { + setState(() { + provider.formItem.dueDate = value; + }); + }, + decoration: InputDecoration( + labelText: + provider.formItem.dueDate == null + ? '选择截止日期' + : '截止: ${DateFormat('yyyy-MM-dd').format(provider.formItem.dueDate!)}', + labelStyle: TextStyle( + color: + provider.formItem.dueDate == null ? Colors.grey : Colors.black87, + ), + prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colors.primary), + ), + filled: true, + fillColor: colors.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + suffixIcon: + provider.formItem.dueDate != null + ? buildClearDueDateSuffixIcon(provider) + : null, + ), + validator: (value) { + if (value != null && + value.isBefore(DateTime.now().subtract(Duration(days: 1)))) { + return '不能选择过去的日期'; + } + return null; + }, + ); + } + + IconButton buildClearScheduledTimeSuffixIcon(TodoProvider provider) { + return IconButton( + icon: Icon(Icons.clear, size: 18), + onPressed: () { + setState(() { + provider.formItem.scheduledTime = null; + }); + widget.formKey.currentState?.fields['scheduledTime']?.didChange(null); + }, + ); + } + + FormBuilderDateTimePicker buildScheduledTimeField(TodoProvider provider) { + final colors = Theme.of(context).colorScheme; + + return FormBuilderDateTimePicker( + name: 'scheduledTime', + format: DateFormat('yyyy-MM-dd HH:mm:ss'), + initialValue: provider.formItem.scheduledTime, + inputType: InputType.both, + onChanged: (value) { + setState(() { + provider.formItem.scheduledTime = value; + }); + }, + decoration: InputDecoration( + labelText: + provider.formItem.scheduledTime == null + ? '选择提醒日期' + : '提醒: ${DateFormat('yyyy-MM-dd HH:mm:ss').format(provider.formItem.scheduledTime!)}', + labelStyle: TextStyle( + color: + provider.formItem.scheduledTime == null + ? Colors.grey + : Colors.black87, + ), + prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colors.primary), + ), + filled: true, + fillColor: colors.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + suffixIcon: + provider.formItem.scheduledTime != null + ? buildClearScheduledTimeSuffixIcon(provider) + : null, + ), + validator: (value) { + if (value != null && value.isBefore(DateTime.now())) { + return '不能选择过去的日期'; + } + return null; + }, + ); + } + + FormBuilderRadioGroup builderRadioGroup(TodoProvider provider) { + return FormBuilderRadioGroup( + name: 'priority', + initialValue: provider.formItem.priority, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + orientation: OptionsOrientation.horizontal, + wrapSpacing: 6, + options: + TodoPriority.values.map((priority) { + return FormBuilderFieldOption( + value: priority, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [Text(priority.label)], + ), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + provider.formItem.priority = value; + }); + } + }, + ); + } + + Container buildPriorityField(TodoProvider provider) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + ), + padding: EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.flag, color: Colors.orange), + SizedBox(width: 6), + Text('优先级'), + ], + ), + builderRadioGroup(provider), + ], + ), + ); + } + + FormBuilder buildForm(TodoProvider provider) { + return FormBuilder( + key: widget.formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + buildTitleField(provider), + SizedBox(height: 12), + buildContentField(provider), + SizedBox(height: 12), + buildDueDateField(provider), + SizedBox(height: 12), + buildScheduledTimeField(provider), + SizedBox(height: 12), + buildPriorityField(provider), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); return Padding( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10), child: Column( mainAxisSize: MainAxisSize.min, - children: [buildTitle(), SizedBox(height: 20), buildForm()], + children: [buildTitle(), SizedBox(height: 20), buildForm(provider)], ), ); } diff --git a/lib/widgets/todo_widget.dart b/lib/widgets/todo_widget.dart index abfbc2e..6ecfbc4 100644 --- a/lib/widgets/todo_widget.dart +++ b/lib/widgets/todo_widget.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; // 统计卡片 -Widget buildTodoStatsCard(List todos) { +Widget buildTodoStatsCard(BuildContext context, List todos) { int totalCount = todos.length; int activeCount = todos.where((todo) => !todo.isCompleted).length; @@ -17,15 +17,15 @@ Widget buildTodoStatsCard(List todos) { mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildStatItem('总计', totalCount, Colors.blue), - _buildStatItem('待完成', activeCount, Colors.orange), - _buildStatItem('已完成', completedCount, Colors.green), + _buildStatItem(context, '总计', totalCount), + _buildStatItem(context, '待完成', activeCount), + _buildStatItem(context, '已完成', completedCount), ], ), ); } -Widget _buildStatItem(String label, int count, Color color) { +Widget _buildStatItem(BuildContext context, String label, int count) { return Column( children: [ Text( @@ -33,7 +33,7 @@ Widget _buildStatItem(String label, int count, Color color) { style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: color, + color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 4), diff --git a/lib/widgets/year_selector.dart b/lib/widgets/year_selector.dart index ec79d3b..f81136d 100644 --- a/lib/widgets/year_selector.dart +++ b/lib/widgets/year_selector.dart @@ -104,7 +104,7 @@ class _YearSelectorState extends State style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + color: Theme.of(context).colorScheme.primary, ), ), ),