Files
AI-Health/health_app/lib/pages/medication/medication_edit_page.dart
MingNian 335a3e6440 feat: 饮食记录逻辑重构 + 通知管理分隔线修复 + 多页面 UI 调整
- 饮食: 新增 DietCommentaryPolicy + diet_record_logic, 饮食记录逻辑抽取复用
- 通知管理: 分隔线改为仿通知中心样式, 跳过图标区域只留文字下方
- 侧边栏: 对话记录操作栏浮层修复 + 常用功能间距收紧
- 智能体: 欢迎卡片去 400ms 延迟 + 胶囊去阴影
- 后端: AI 会话仓库新增方法 + 饮食评论策略 + 健康记录规则调整
- 其他: api_client IP 适配 + 多页面 UI 微调 + 新增测试
2026-07-12 22:49:38 +08:00

421 lines
12 KiB
Dart
Raw 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.

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_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<MedicationEditPage> createState() => _MedicationEditPageState();
}
class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
final _nameCtrl = TextEditingController();
final _dosageCtrl = TextEditingController();
final _notesCtrl = TextEditingController();
int _timesPerDay = 2;
List<TimeOfDay> _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<void> _load() async {
final srv = ref.read(medicationServiceProvider);
final list = await srv.getList();
final m = list.cast<Map<String, dynamic>?>().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<void> _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: AppColors.border, 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),
_GradientOutlineButton(
onPressed: _loading ? null : _save,
label: _loading ? '保存中...' : '保存',
),
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(AppColors.primary),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
),
style: AppTextStyles.formValue,
),
],
);
Widget _dateField(String label, DateTime val, ValueChanged<DateTime> 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<DateTime?> 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,
);
}
}
class _GradientOutlineButton extends StatelessWidget {
final VoidCallback? onPressed;
final String label;
const _GradientOutlineButton({required this.onPressed, required this.label});
@override
Widget build(BuildContext context) {
final enabled = onPressed != null;
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: enabled ? AppColors.actionOutlineGradient : null,
color: enabled ? null : AppColors.border,
borderRadius: AppRadius.lgBorder,
),
padding: const EdgeInsets.all(1.4),
child: Material(
color: Colors.white,
borderRadius: AppRadius.lgBorder,
child: InkWell(
onTap: onPressed,
borderRadius: AppRadius.lgBorder,
child: SizedBox(
height: 46,
child: Center(
child: enabled
? ShaderMask(
shaderCallback: (bounds) =>
AppColors.actionOutlineGradient.createShader(bounds),
child: Text(
label,
style: AppTextStyles.button.copyWith(
color: Colors.white,
),
),
)
: Text(
label,
style: AppTextStyles.button.copyWith(
color: AppColors.textHint,
),
),
),
),
),
),
);
}
}
String _displayDate(DateTime date) {
return '${date.year}/${date.month.toString().padLeft(2, '0')}/${date.day.toString().padLeft(2, '0')}';
}