import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/app_colors.dart'; import '../../core/app_design_tokens.dart'; import '../../core/app_module_visuals.dart'; import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/data_providers.dart'; import '../../widgets/app_toast.dart'; class MedicationEditPage extends ConsumerStatefulWidget { final String? id; const MedicationEditPage({super.key, this.id}); @override ConsumerState createState() => _MedicationEditPageState(); } class _MedicationEditPageState extends ConsumerState { static const _visual = AppModuleVisuals.medication; final _nameCtrl = TextEditingController(); final _dosageCtrl = TextEditingController(); final _notesCtrl = TextEditingController(); int _timesPerDay = 2; List _times = [ const TimeOfDay(hour: 8, minute: 0), const TimeOfDay(hour: 18, minute: 0), ]; DateTime _start = DateTime.now(); DateTime? _end; bool _loading = false; @override void initState() { super.initState(); if (widget.id != null) _load(); } @override void dispose() { _nameCtrl.dispose(); _dosageCtrl.dispose(); _notesCtrl.dispose(); super.dispose(); } Future _load() async { final srv = ref.read(medicationServiceProvider); final list = await srv.getList(); final m = list.cast?>().firstWhere( (x) => x?['id']?.toString() == widget.id, orElse: () => null, ); if (m == null || !mounted) return; _nameCtrl.text = m['name']?.toString() ?? ''; _dosageCtrl.text = m['dosage']?.toString() ?? ''; _notesCtrl.text = m['notes']?.toString() ?? ''; final tod = (m['timeOfDay'] as List?)?.map((t) { final parts = t.toString().split(':'); return TimeOfDay( hour: int.tryParse(parts[0]) ?? 8, minute: int.tryParse(parts.length > 1 ? parts[1] : '0') ?? 0, ); }).toList() ?? [ const TimeOfDay(hour: 8, minute: 0), const TimeOfDay(hour: 18, minute: 0), ]; _timesPerDay = tod.length; _times = tod; if (m['startDate'] != null) { _start = DateTime.tryParse(m['startDate'].toString()) ?? DateTime.now(); } if (m['endDate'] != null) _end = DateTime.tryParse(m['endDate'].toString()); setState(() {}); } Future _save() async { final name = _nameCtrl.text.trim(); if (name.isEmpty) { AppToast.show(context, '请输入药品名称', type: AppToastType.warning); return; } setState(() => _loading = true); final data = { 'name': name, 'dosage': _dosageCtrl.text.trim(), 'frequency': 'Daily', 'notes': _notesCtrl.text.trim(), 'timeOfDay': _times .map( (t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}', ) .toList(), 'startDate': '${_start.year}-${_start.month.toString().padLeft(2, '0')}-${_start.day.toString().padLeft(2, '0')}', if (_end != null) 'endDate': '${_end!.year}-${_end!.month.toString().padLeft(2, '0')}-${_end!.day.toString().padLeft(2, '0')}', 'source': 'Manual', }; final srv = ref.read(medicationServiceProvider); try { if (widget.id != null) { await srv.update(widget.id!, data); } else { await srv.create(data); } ref.invalidate(medicationListProvider); ref.invalidate(medicationReminderProvider); if (mounted) popRoute(ref); } catch (_) { if (mounted) { AppToast.show(context, '保存失败', type: AppToastType.error); } } if (mounted) setState(() => _loading = false); } void _updateTimes(int n) { setState(() { _timesPerDay = n; while (_times.length < n) { _times.add(const TimeOfDay(hour: 12, minute: 0)); } while (_times.length > n) { _times.removeLast(); } }); } @override Widget build(BuildContext context) { return GradientScaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref), ), title: Text(widget.id != null ? '编辑用药' : '添加用药'), ), body: ListView( padding: const EdgeInsets.all(16), children: [ Row( children: [ Expanded( flex: 3, child: _field('药品名称', _nameCtrl, hint: '如:阿司匹林'), ), const SizedBox(width: 12), Expanded( flex: 2, child: _field('剂量', _dosageCtrl, hint: '如:100mg'), ), ], ), const SizedBox(height: 16), _label('每日服药次数'), const SizedBox(height: 8), GestureDetector( onTap: () async { final n = await showAppCountPicker( context, initialValue: _timesPerDay, min: 1, max: 4, label: '次', ); if (n != null) _updateTimes(n); }, child: _PickerBox( child: Text('$_timesPerDay 次/天', style: AppTextStyles.formValue), ), ), const SizedBox(height: 16), _label('服药时间'), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: List.generate( _timesPerDay, (i) => GestureDetector( onTap: () async { final t = await showAppTimePicker( context, initialTime: _times[i], ); if (t != null) setState(() => _times[i] = t); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10, ), decoration: BoxDecoration( color: Colors.white, borderRadius: AppRadius.mdBorder, border: Border.all(color: _visual.borderColor, width: 1), ), child: Text( '${_times[i].hour.toString().padLeft(2, '0')}:${_times[i].minute.toString().padLeft(2, '0')}', style: AppTextStyles.formValue, ), ), ), ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: _dateField( '开始日期', _start, (d) => setState(() => _start = d), ), ), const SizedBox(width: 12), Expanded( child: _dateFieldOpt( '结束日期(可选)', _end, (d) => setState(() => _end = d), ), ), ], ), const SizedBox(height: 16), _field('备注', _notesCtrl, hint: '如:饭后服用、睡前'), const SizedBox(height: 32), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _loading ? null : _save, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: _visual.color, side: BorderSide(color: _visual.color, width: 1.5), shape: RoundedRectangleBorder(borderRadius: AppRadius.lgBorder), padding: const EdgeInsets.symmetric(vertical: 14), ), child: Text(_loading ? '保存中...' : '保存', style: AppTextStyles.button), ), ), const SizedBox(height: 20), ], ), ); } Widget _label(String text) => Text(text, style: AppTextStyles.formLabel); Widget _field(String label, TextEditingController ctrl, {String? hint}) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _label(label), const SizedBox(height: 6), TextField( controller: ctrl, decoration: InputDecoration( hintText: hint, filled: true, fillColor: AppTheme.surface, border: _inputBorder(AppColors.border), enabledBorder: _inputBorder(AppColors.border), focusedBorder: _inputBorder(_visual.color), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 12, ), ), style: AppTextStyles.formValue, ), ], ); Widget _dateField(String label, DateTime val, ValueChanged cb) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _label(label), const SizedBox(height: 6), GestureDetector( onTap: () async { final d = await showAppDatePicker( context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030), ); if (d != null) cb(d); }, child: _PickerBox( child: Text(_displayDate(val), style: AppTextStyles.formValue), ), ), ], ); Widget _dateFieldOpt( String label, DateTime? val, ValueChanged cb, ) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _label(label), const SizedBox(height: 6), GestureDetector( onTap: () async { final d = await showAppDatePicker( context, initialDate: val ?? DateTime.now(), firstDate: DateTime(2024), lastDate: DateTime(2030), ); if (d != null) cb(d); }, child: _PickerBox( child: Row( children: [ Expanded( child: Text( val != null ? _displayDate(val) : '不设置', style: AppTextStyles.formValue.copyWith( color: val != null ? null : AppTheme.textHint, ), ), ), if (val != null) GestureDetector( onTap: () => cb(null), child: const Icon( Icons.close, size: 19, color: AppColors.textHint, ), ), ], ), ), ), ], ); OutlineInputBorder _inputBorder(Color color) => OutlineInputBorder( borderRadius: AppRadius.mdBorder, borderSide: BorderSide(color: color), ); } class _PickerBox extends StatelessWidget { final Widget child; const _PickerBox({required this.child}); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.surface, borderRadius: AppRadius.mdBorder, border: Border.all(color: AppColors.border), ), child: child, ); } } String _displayDate(DateTime date) { return '${date.year}/${date.month.toString().padLeft(2, '0')}/${date.day.toString().padLeft(2, '0')}'; }