import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/network/api_exception.dart'; import '../../../providers/add_bill_provider.dart'; import '../../../providers/category_provider.dart'; import '../../../utils/amount_input_formatter.dart'; import '../../../utils/date_util.dart'; import '../../../utils/toast.dart'; import '../../../widgets/category_picker_grid.dart'; import '../../../widgets/state_views.dart'; /// 手动记账表单(复刻 uniApp add.vue 手动 Tab) class ManualEntryTab extends ConsumerStatefulWidget { const ManualEntryTab({super.key}); @override ConsumerState createState() => _ManualEntryTabState(); } class _ManualEntryTabState extends ConsumerState { late final TextEditingController _descriptionController = TextEditingController(); late final TextEditingController _amountController = TextEditingController(); @override void dispose() { _descriptionController.dispose(); _amountController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final form = ref.watch(addBillFormProvider); final notifier = ref.read(addBillFormProvider.notifier); // OCR「选择此条」外部填充时,同步非受控输入框 ref.listen(addBillFormProvider.select((s) => s.revision), (prev, next) { if (prev == next) return; final s = ref.read(addBillFormProvider); _amountController.text = s.amount?.toStringAsFixed(2) ?? ''; _descriptionController.text = s.description; }); return ListView( padding: const EdgeInsets.all(16), children: [ _sectionLabel('账单类型'), _typeSelector(form, notifier), const SizedBox(height: 16), _sectionLabel('分类'), _categoryGrid(form, notifier), const SizedBox(height: 16), _sectionLabel('金额'), _amountField(form, notifier), const SizedBox(height: 16), _sectionLabel('日期'), _dateField(context, form, notifier), const SizedBox(height: 16), _sectionLabel('备注'), _descriptionField(form, notifier), const SizedBox(height: 24), _submitBtn(context, ref, form, notifier), const SizedBox(height: 90), // 底部导航栏避让 ], ); } Widget _sectionLabel(String text) => Padding( padding: const EdgeInsets.only(bottom: 8, left: 4), child: Text( text, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5D4037)), ), ); // ---- 类型选择 ---- Widget _typeSelector(AddBillFormState form, AddBillFormNotifier notifier) { return Row( children: [ _typeCard('📤', '支出', 1, form, notifier), const SizedBox(width: 10), _typeCard('📥', '收入', 2, form, notifier), ], ); } Widget _typeCard(String icon, String label, int type, AddBillFormState form, AddBillFormNotifier notifier) { final active = form.type == type; return Expanded( child: GestureDetector( onTap: () => notifier.setType(type), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: active ? Colors.white : Colors.white.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(14), border: Border.all( color: active ? const Color(0xFFC8956E) : Colors.transparent, width: 2, ), boxShadow: active ? [BoxShadow(color: const Color(0x33C8956E), blurRadius: 8, offset: const Offset(0, 3))] : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(icon, style: const TextStyle(fontSize: 20)), const SizedBox(width: 8), Text( label, style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: active ? const Color(0xFF5D4037) : const Color(0xFF8D6E63), ), ), ], ), ), ), ); } // ---- 分类宫格 ---- Widget _categoryGrid(AddBillFormState form, AddBillFormNotifier notifier) { return Consumer(builder: (context, ref, _) { final categoriesAsync = ref.watch(categoriesProvider(form.type)); return categoriesAsync.when( loading: () => const LoadingView(padding: EdgeInsets.all(20)), error: (e, _) => ErrorView( message: '分类加载失败:${asApiException(e).message}', onRetry: () => ref.invalidate(categoriesProvider), ), data: (categories) => CategoryPickerGrid( categories: categories, selectedId: form.categoryId, onSelect: notifier.setCategory, crossAxisCount: 5, ), ); }); } // ---- 金额 ---- Widget _amountField(AddBillFormState form, AddBillFormNotifier notifier) { return TextField( controller: _amountController, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [AmountInputFormatter()], style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF5D4037)), decoration: InputDecoration( hintText: '请输入金额', prefixText: '¥ ', prefixStyle: const TextStyle(fontSize: 18, color: Color(0xFFC8956E), fontWeight: FontWeight.bold), filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none), ), onChanged: (v) => notifier.setAmount(double.tryParse(v)), ); } // ---- 日期 ---- Widget _dateField(BuildContext context, AddBillFormState form, AddBillFormNotifier notifier) { return GestureDetector( onTap: () async { final initial = DateTime.tryParse(form.billDate) ?? DateTime.now(); final picked = await showDatePicker( context: context, initialDate: initial, firstDate: DateTime(2000), lastDate: DateTime(2100), ); if (picked != null) notifier.setDate(formatDate(picked)); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), ), child: Row( children: [ Text(form.billDate, style: const TextStyle(fontSize: 15, color: Color(0xFF5D4037))), const Spacer(), const Icon(Icons.chevron_right, color: Color(0xFF8D6E63)), ], ), ), ); } // ---- 备注 ---- Widget _descriptionField(AddBillFormState form, AddBillFormNotifier notifier) { return TextField( maxLines: 3, maxLength: 200, controller: _descriptionController, style: const TextStyle(fontSize: 15, color: Color(0xFF5D4037)), decoration: InputDecoration( hintText: '请输入备注(可选)', filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none), ), onChanged: notifier.setDescription, ); } // ---- 保存 ---- Widget _submitBtn(BuildContext context, WidgetRef ref, AddBillFormState form, AddBillFormNotifier notifier) { return SizedBox( width: double.infinity, height: 50, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: const Color(0xFFC8956E), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), ), onPressed: form.submitting ? null : () async { try { await notifier.submit(); _amountController.clear(); _descriptionController.clear(); if (context.mounted) showToast(context, '✅ 记账成功'); } on ApiException catch (e) { if (context.mounted) showToast(context, e.message, error: true); } catch (_) { if (context.mounted) showToast(context, '保存失败,请重试', error: true); } }, child: form.submitting ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white), ) : const Text('保 存', style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold, letterSpacing: 4)), ), ); } }