- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层 - 修复所有相对路径导入错误,统一调整为扁平化模块引用 - 更新多平台 pubspec 版本号与依赖库版本 - 补充后端功能问题管理后台与脚本工具 - 调整部分页面的快捷方式文案适配新功能 - 更新部分翻译覆盖率与API文档
912 lines
27 KiB
Dart
912 lines
27 KiB
Dart
// ============================================================
|
||
// 闲言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),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|