Files
xianyan/lib/features/progress/presentation/progress_sheets.dart
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

912 lines
27 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 闲言APP — 进度添加/编辑面板
// 创建时间: 2026-05-30
// 更新时间: 2026-05-30
// 作用: 进度页面的添加/编辑面板 — 表单/样式选择/日期选择
// 上次更新: 移除ext构造参数改为从context获取AppThemeExtension
// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_radius.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_typography.dart';
import '../../../shared/widgets/adaptive/keyboard_safe_sheet.dart';
import '../../../shared/widgets/feedback/app_toast.dart';
import '../progress_models.dart';
import '../progress_provider.dart';
String progressStyleDescription(ProgressDisplayStyle style) {
return switch (style) {
ProgressDisplayStyle.progressBar => '横向进度条,直观展示完成比例',
ProgressDisplayStyle.ringProgress => '圆环样式,适合百分比展示',
ProgressDisplayStyle.countdownGrid => '天/时/分/秒网格,实时倒数',
ProgressDisplayStyle.tagOnly => '简洁标签展示,适合已过期项目',
};
}
/// 进度模板
class ProgressTemplate {
const ProgressTemplate({
required this.emoji,
required this.name,
required this.typeLabel,
this.tagText,
this.daysOffset = 30,
});
final String emoji;
final String name;
final String typeLabel;
final String? tagText;
final int daysOffset;
static const presets = [
ProgressTemplate(
emoji: '🎓',
name: '考研倒计时',
typeLabel: '倒计时',
tagText: '冲刺备考',
daysOffset: 180,
),
ProgressTemplate(
emoji: '🎂',
name: '生日倒计时',
typeLabel: '倒计时',
tagText: '期待生日',
daysOffset: 90,
),
ProgressTemplate(
emoji: '🎯',
name: '年底目标',
typeLabel: '进度',
tagText: '年度目标',
daysOffset: 180,
),
ProgressTemplate(
emoji: '🚀',
name: '项目上线',
typeLabel: '倒计时',
tagText: '冲刺上线',
daysOffset: 60,
),
ProgressTemplate(
emoji: '💍',
name: '婚礼倒计时',
typeLabel: '倒计时',
tagText: '幸福倒计时',
daysOffset: 120,
),
ProgressTemplate(
emoji: '✈️',
name: '旅行倒计时',
typeLabel: '倒计时',
tagText: '出发在即',
daysOffset: 45,
),
ProgressTemplate(
emoji: '📚',
name: '读书计划',
typeLabel: '进度',
tagText: '坚持阅读',
daysOffset: 90,
),
ProgressTemplate(
emoji: '💪',
name: '健身打卡',
typeLabel: '进度',
tagText: '自律即自由',
daysOffset: 60,
),
];
}
class ProgressAddSheet extends ConsumerStatefulWidget {
const ProgressAddSheet({super.key, this.prefillName, this.onAdded});
final String? prefillName;
final VoidCallback? onAdded;
static void show(
BuildContext context, {
String? prefillName,
VoidCallback? onAdded,
}) {
showCupertinoModalPopup<void>(
context: context,
builder: (_) =>
ProgressAddSheet(prefillName: prefillName, onAdded: onAdded),
);
}
@override
ConsumerState<ProgressAddSheet> createState() => _ProgressAddSheetState();
}
class _ProgressAddSheetState extends ConsumerState<ProgressAddSheet> {
late final TextEditingController _nameCtrl;
late final TextEditingController _tagCtrl;
late final TextEditingController _noteCtrl;
late DateTime _selectedDate;
late String _selectedType;
late ProgressDisplayStyle _selectedStyle;
@override
void initState() {
super.initState();
_nameCtrl = TextEditingController(text: widget.prefillName ?? '');
_tagCtrl = TextEditingController();
_noteCtrl = TextEditingController();
_selectedDate = DateTime.now().add(const Duration(days: 30));
_selectedType = '倒计时';
_selectedStyle = ProgressDisplayStyle.countdownGrid;
}
@override
void dispose() {
_nameCtrl.dispose();
_tagCtrl.dispose();
_noteCtrl.dispose();
super.dispose();
}
void _applyTemplate(ProgressTemplate tmpl) {
_nameCtrl.text = tmpl.name;
_tagCtrl.text = tmpl.tagText ?? '';
_selectedType = tmpl.typeLabel;
_selectedDate = DateTime.now().add(Duration(days: tmpl.daysOffset));
_selectedStyle = tmpl.typeLabel.contains('进度')
? ProgressDisplayStyle.progressBar
: ProgressDisplayStyle.countdownGrid;
setState(() {});
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return KeyboardSafeBuilder(
builder: (ctx, _) => Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(ctx).size.height * 0.75,
),
decoration: BoxDecoration(
color: ext.bgPrimary,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppRadius.xl),
),
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildDragHandle(ext),
_buildHeader(ext),
const SizedBox(height: AppSpacing.md),
_buildTemplatePicker(ext),
const SizedBox(height: AppSpacing.sm),
ProgressSheetField(
label: '名称',
hint: '如:考研倒计时',
controller: _nameCtrl,
),
const SizedBox(height: AppSpacing.sm),
ProgressSheetField(
label: '标签',
hint: '如:加油冲刺',
controller: _tagCtrl,
),
const SizedBox(height: AppSpacing.sm),
ProgressSheetField(
label: '笔记',
hint: '添加备注...',
controller: _noteCtrl,
),
const SizedBox(height: AppSpacing.sm),
_buildTypeSelector(ext),
const SizedBox(height: AppSpacing.sm),
_buildDatePicker(ext),
const SizedBox(height: AppSpacing.sm),
ProgressDisplayStyleSelector(
selected: _selectedStyle,
onChanged: (style) => setState(() => _selectedStyle = style),
),
const SizedBox(height: AppSpacing.lg),
_buildAddButton(ext),
const SizedBox(height: AppSpacing.md),
],
),
),
),
),
);
}
Widget _buildDragHandle(AppThemeExtension ext) {
return Container(
margin: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
width: 36,
height: 4,
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
);
}
Widget _buildHeader(AppThemeExtension ext) {
return Row(
children: [
Icon(CupertinoIcons.plus, size: 20, color: ext.accent),
const SizedBox(width: 6),
Text(
'添加进度',
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: AppTypography.body.copyWith(color: ext.textSecondary),
),
),
],
);
}
Widget _buildTemplatePicker(AppThemeExtension ext) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'📋 快捷模板',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
const SizedBox(height: AppSpacing.xs),
SizedBox(
height: 36,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: ProgressTemplate.presets.length,
separatorBuilder: (_, __) => const SizedBox(width: 6),
itemBuilder: (_, index) {
final tmpl = ProgressTemplate.presets[index];
return GestureDetector(
onTap: () => _applyTemplate(tmpl),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
border: Border.all(color: ext.overlaySubtle, width: 0.5),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(tmpl.emoji, style: const TextStyle(fontSize: 14)),
const SizedBox(width: 4),
Text(
tmpl.name,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
],
),
),
);
},
),
),
],
);
}
Widget _buildTypeSelector(AppThemeExtension ext) {
return Row(
children: [
Text(
'类型',
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
),
const Spacer(),
CupertinoSlidingSegmentedControl<String>(
groupValue: _selectedType,
onValueChanged: (v) {
if (v != null) {
setState(() {
_selectedType = v;
_selectedStyle = v.contains('进度')
? ProgressDisplayStyle.progressBar
: ProgressDisplayStyle.countdownGrid;
});
}
},
children: {
'倒计时': Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.timer,
size: 14,
color: _selectedType == '倒计时'
? ext.textOnAccent
: ext.textSecondary,
),
const SizedBox(width: 4),
const Text('倒计时'),
],
),
),
'进度': Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.chart_bar_fill,
size: 14,
color: _selectedType == '进度'
? ext.textOnAccent
: ext.textSecondary,
),
const SizedBox(width: 4),
const Text('进度'),
],
),
),
},
),
],
);
}
Widget _buildDatePicker(AppThemeExtension ext) {
return Row(
children: [
Text(
'目标日期',
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
),
const Spacer(),
CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () async {
final picked = await showCupertinoModalPopup<DateTime>(
context: context,
builder: (pickerCtx) => Container(
height: 260,
color: ext.bgPrimary,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: _selectedDate,
minimumDate: DateTime.now(),
onDateTimeChanged: (d) {
setState(() => _selectedDate = d);
},
),
),
);
if (picked != null) {
setState(() => _selectedDate = picked);
}
},
child: Text(
'${_selectedDate.year}-${_selectedDate.month.toString().padLeft(2, '0')}-${_selectedDate.day.toString().padLeft(2, '0')}',
style: AppTypography.callout.copyWith(color: ext.accent),
),
),
],
);
}
Widget _buildAddButton(AppThemeExtension ext) {
return SizedBox(
width: double.infinity,
child: CupertinoButton.filled(
onPressed: () {
if (_nameCtrl.text.trim().isEmpty) return;
ref
.read(progressProvider.notifier)
.addUserItem(
name: _nameCtrl.text.trim(),
targetDate: _selectedDate,
typeLabel: _selectedType,
tagText: _tagCtrl.text.trim().isEmpty
? null
: _tagCtrl.text.trim(),
note: _noteCtrl.text.trim().isEmpty
? null
: _noteCtrl.text.trim(),
);
Navigator.pop(context);
widget.onAdded?.call();
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
CupertinoIcons.checkmark,
size: 16,
color: CupertinoColors.white,
),
const SizedBox(width: 6),
Text(
'添加',
style: AppTypography.callout.copyWith(
color: CupertinoColors.white,
),
),
],
),
),
);
}
}
class ProgressEditSheet extends ConsumerStatefulWidget {
const ProgressEditSheet({super.key, required this.item, this.onUpdated});
final ProgressItem item;
final VoidCallback? onUpdated;
static void show(
BuildContext context, {
required ProgressItem item,
VoidCallback? onUpdated,
}) {
showCupertinoModalPopup<void>(
context: context,
builder: (_) => ProgressEditSheet(item: item, onUpdated: onUpdated),
);
}
@override
ConsumerState<ProgressEditSheet> createState() => _ProgressEditSheetState();
}
class _ProgressEditSheetState extends ConsumerState<ProgressEditSheet> {
late final TextEditingController _nameCtrl;
late final TextEditingController _tagCtrl;
late DateTime _selectedDate;
late ProgressDisplayStyle _selectedStyle;
@override
void initState() {
super.initState();
_nameCtrl = TextEditingController(text: widget.item.title);
_tagCtrl = TextEditingController(text: widget.item.tagText ?? '');
_selectedDate =
widget.item.targetDate ?? DateTime.now().add(const Duration(days: 30));
_selectedStyle = widget.item.displayStyle;
}
@override
void dispose() {
_nameCtrl.dispose();
_tagCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return KeyboardSafeBuilder(
builder: (ctx, _) => Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(ctx).size.height * 0.75,
),
decoration: BoxDecoration(
color: ext.bgPrimary,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppRadius.xl),
),
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildDragHandle(ext),
_buildHeader(ext),
const SizedBox(height: AppSpacing.md),
ProgressSheetField(
label: '名称',
hint: '进度名称',
controller: _nameCtrl,
),
const SizedBox(height: AppSpacing.sm),
ProgressSheetField(
label: '标签',
hint: '标签文字',
controller: _tagCtrl,
),
const SizedBox(height: AppSpacing.sm),
_buildDatePicker(ext),
const SizedBox(height: AppSpacing.sm),
ProgressDisplayStyleSelector(
selected: _selectedStyle,
onChanged: (style) => setState(() => _selectedStyle = style),
),
const SizedBox(height: AppSpacing.lg),
_buildSaveButton(ext),
const SizedBox(height: AppSpacing.md),
],
),
),
),
),
);
}
Widget _buildDragHandle(AppThemeExtension ext) {
return Container(
margin: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
width: 36,
height: 4,
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
);
}
Widget _buildHeader(AppThemeExtension ext) {
return Row(
children: [
Icon(CupertinoIcons.pencil, size: 20, color: ext.accent),
const SizedBox(width: 6),
Text(
'编辑进度',
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: AppTypography.body.copyWith(color: ext.textSecondary),
),
),
],
);
}
Widget _buildDatePicker(AppThemeExtension ext) {
return Row(
children: [
Text(
'目标日期',
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
),
const Spacer(),
CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () async {
final picked = await showCupertinoModalPopup<DateTime>(
context: context,
builder: (pickerCtx) => Container(
height: 260,
color: ext.bgPrimary,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: _selectedDate,
minimumDate: DateTime.now(),
onDateTimeChanged: (d) {
setState(() => _selectedDate = d);
},
),
),
);
if (picked != null) {
setState(() => _selectedDate = picked);
}
},
child: Text(
'${_selectedDate.year}-${_selectedDate.month.toString().padLeft(2, '0')}-${_selectedDate.day.toString().padLeft(2, '0')}',
style: AppTypography.callout.copyWith(color: ext.accent),
),
),
],
);
}
Widget _buildSaveButton(AppThemeExtension ext) {
return SizedBox(
width: double.infinity,
child: CupertinoButton.filled(
onPressed: () {
if (_nameCtrl.text.trim().isEmpty) return;
if (_selectedDate.isBefore(DateTime.now())) {
AppToast.showInfo('目标日期不能早于当前日期');
return;
}
ref
.read(progressProvider.notifier)
.updateUserItem(
id: widget.item.id,
name: _nameCtrl.text.trim(),
targetDate: _selectedDate,
tagText: _tagCtrl.text.trim().isEmpty
? null
: _tagCtrl.text.trim(),
displayStyle: _selectedStyle,
);
Navigator.pop(context);
AppToast.showSuccess('✅ 已更新');
widget.onUpdated?.call();
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
CupertinoIcons.checkmark,
size: 16,
color: CupertinoColors.white,
),
const SizedBox(width: 6),
Text(
'保存',
style: AppTypography.callout.copyWith(
color: CupertinoColors.white,
),
),
],
),
),
);
}
}
class ProgressSheetField extends StatelessWidget {
const ProgressSheetField({
super.key,
required this.label,
required this.hint,
required this.controller,
});
final String label;
final String hint;
final TextEditingController controller;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return Row(
children: [
SizedBox(
width: 64,
child: Text(
label,
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
),
),
Expanded(
child: CupertinoTextField(
controller: controller,
placeholder: hint,
placeholderStyle: AppTypography.subhead.copyWith(
color: ext.textHint,
),
style: AppTypography.body.copyWith(color: ext.textPrimary),
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
),
),
],
);
}
}
class ProgressDisplayStyleSelector extends StatelessWidget {
const ProgressDisplayStyleSelector({
super.key,
required this.selected,
required this.onChanged,
});
final ProgressDisplayStyle selected;
final ValueChanged<ProgressDisplayStyle> onChanged;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'显示样式',
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
),
const SizedBox(height: AppSpacing.sm),
Row(
children: ProgressDisplayStyle.values.map((style) {
final isSelected = style == selected;
return Expanded(
child: GestureDetector(
onTap: () => onChanged(style),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
margin: const EdgeInsets.symmetric(horizontal: 3),
padding: const EdgeInsets.symmetric(
vertical: AppSpacing.sm,
horizontal: AppSpacing.xs,
),
decoration: BoxDecoration(
color: isSelected
? ext.accent.withValues(alpha: 0.15)
: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
border: isSelected
? Border.all(color: ext.accent, width: 1.5)
: null,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ProgressStylePreview(
style: style,
isSelected: isSelected,
),
const SizedBox(height: 4),
Text(style.emoji, style: const TextStyle(fontSize: 14)),
Text(
style.label,
style: AppTypography.caption2.copyWith(
color: isSelected ? ext.accent : ext.textHint,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.normal,
),
),
],
),
),
),
);
}).toList(),
),
],
);
}
}
class ProgressStylePreview extends StatelessWidget {
const ProgressStylePreview({
super.key,
required this.style,
required this.isSelected,
});
final ProgressDisplayStyle style;
final bool isSelected;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final color = isSelected ? ext.accent : ext.textHint;
switch (style) {
case ProgressDisplayStyle.progressBar:
return SizedBox(
width: 48,
height: 8,
child: ClipRRect(
borderRadius: AppRadius.fullBorder,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.fullBorder,
),
),
Align(
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: 0.65,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [ext.accent, ext.accentLight],
),
borderRadius: AppRadius.fullBorder,
),
),
),
),
],
),
),
);
case ProgressDisplayStyle.ringProgress:
return SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(
value: 0.65,
strokeWidth: 3,
backgroundColor: ext.bgSecondary,
color: color,
strokeCap: StrokeCap.round,
),
);
case ProgressDisplayStyle.countdownGrid:
return const Row(
mainAxisSize: MainAxisSize.min,
children: [
ProgressMiniCountdownCell(value: '0'),
SizedBox(width: 2),
ProgressMiniCountdownCell(value: '0'),
],
);
case ProgressDisplayStyle.tagOnly:
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: AppRadius.fullBorder,
),
child: const Text('🏷️', style: TextStyle(fontSize: 10)),
);
}
}
}
class ProgressMiniCountdownCell extends StatelessWidget {
const ProgressMiniCountdownCell({super.key, required this.value});
final String value;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: BorderRadius.circular(3),
),
child: Center(
child: Text(
value,
style: const TextStyle(fontSize: 8, fontWeight: FontWeight.w700),
),
),
);
}
}