Files
AI-Health/health_app/lib/pages/medication/medication_edit_page.dart
MingNian e3b9716f7c fix: 图片发送/医生加载/运动超时/用药黑屏/服药打卡
- sendImage: 本地预览→上传→远程URL替换
- doctorListProvider: 8s超时+mock医生fallback
- currentExercisePlanProvider: 8s超时→显示空状态
- 用药编辑: try-catch防黑屏+刷新列表
- 服药打卡: 接入后端confirm()接口
2026-06-03 20:03:17 +08:00

529 lines
17 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/navigation_provider.dart';
import '../../providers/data_providers.dart';
class _MedicationItem {
String name = '';
String dosage = '';
String frequency = '每日1次';
List<TimeOfDay> times = [const TimeOfDay(hour: 8, minute: 0)];
DateTime startDate = DateTime.now();
DateTime? endDate;
int weekday = 1;
}
const _frequencies = ['每日1次', '每日2次', '每日3次', '每周1次', '按需服用'];
const _weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
class MedicationEditPage extends ConsumerStatefulWidget {
final String? medicationId;
const MedicationEditPage({super.key, this.medicationId});
@override
ConsumerState<MedicationEditPage> createState() => _MedicationEditPageState();
}
class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
final _items = <_MedicationItem>[];
final _nameCtrls = <TextEditingController>[];
final _doseCtrls = <TextEditingController>[];
@override
void initState() {
super.initState();
_addItem();
}
@override
void dispose() {
for (final c in _nameCtrls) {
c.dispose();
}
for (final c in _doseCtrls) {
c.dispose();
}
super.dispose();
}
void _addItem() {
setState(() {
_items.add(_MedicationItem());
_nameCtrls.add(TextEditingController());
_doseCtrls.add(TextEditingController());
});
}
void _removeItem(int index) {
setState(() {
_nameCtrls[index].dispose();
_doseCtrls[index].dispose();
_nameCtrls.removeAt(index);
_doseCtrls.removeAt(index);
_items.removeAt(index);
});
}
void _onSave() async {
for (int i = 0; i < _items.length; i++) {
_items[i].name = _nameCtrls[i].text.trim();
_items[i].dosage = _doseCtrls[i].text.trim();
}
final allValid = _items.every(
(item) => item.name.isNotEmpty && item.dosage.isNotEmpty,
);
if (!allValid) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请填写所有药品的名称和剂量')),
);
return;
}
final service = ref.read(medicationServiceProvider);
try {
for (final item in _items) {
final timesStr = item.frequency == '按需服用'
? []
: item.times.map((t) => t.format(context)).toList();
await service.create({
'name': item.name,
'dosage': item.dosage,
'frequency': item.frequency,
'times': timesStr,
'start_date': item.startDate.toIso8601String().split('T')[0],
if (item.endDate != null)
'end_date': item.endDate!.toIso8601String().split('T')[0],
if (item.frequency == '每周1次') 'weekday': item.weekday,
});
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已添加 ${_items.length} 种药品'),
backgroundColor: const Color(0xFF635BFF),
),
);
ref.invalidate(medicationListProvider);
ref.invalidate(medicationReminderProvider);
popRoute(ref);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('保存失败:$e'),
backgroundColor: Colors.red,
),
);
// 仍然返回上一页,避免卡在黑屏
popRoute(ref);
}
}
int _timeCount(String frequency) {
switch (frequency) {
case '每日1次':
return 1;
case '每日2次':
return 2;
case '每日3次':
return 3;
case '每周1次':
return 1;
default:
return 0;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F7FF),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => popRoute(ref),
),
title: const Text(
'添加用药',
style: TextStyle(
color: Color(0xFF1A1A1A),
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
actions: [
TextButton(
onPressed: _onSave,
child: const Text(
'保存',
style: TextStyle(
color: Color(0xFF635BFF),
fontWeight: FontWeight.w600,
),
),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...List.generate(_items.length, (i) => _buildCard(i)),
const SizedBox(height: 12),
_buildAddButton(),
const SizedBox(height: 40),
],
),
),
);
}
Widget _buildCard(int index) {
final item = _items[index];
final count = _timeCount(item.frequency);
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFEEEEEE)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'药品 ${index + 1}',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF635BFF),
),
),
if (_items.length > 1)
GestureDetector(
onTap: () => _removeItem(index),
child: const Icon(Icons.close, size: 18, color: Color(0xFFBDBDBD)),
),
],
),
const SizedBox(height: 8),
Divider(height: 1, color: const Color(0xFFF0F0F0)),
const SizedBox(height: 8),
// Name
_buildLabel('药品名称'),
const SizedBox(height: 4),
TextField(
controller: _nameCtrls[index],
style: const TextStyle(fontSize: 14),
decoration: _inputDecoration('请输入药品名称'),
),
const SizedBox(height: 8),
// Dosage
_buildLabel('剂量'),
const SizedBox(height: 4),
TextField(
controller: _doseCtrls[index],
style: const TextStyle(fontSize: 14),
decoration: _inputDecoration('100mg'),
),
const SizedBox(height: 8),
// Frequency
_buildLabel('服用频率'),
const SizedBox(height: 4),
GestureDetector(
onTap: () => _pickFrequency(index),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE0E0E0)),
color: const Color(0xFFFAFAFA),
),
child: Row(
children: [
Text(item.frequency, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
const Spacer(),
const Icon(Icons.keyboard_arrow_down, size: 20, color: Color(0xFF9E9E9E)),
],
),
),
),
const SizedBox(height: 8),
// Times (dynamic)
if (count > 0) ...[
_buildLabel('服药时间'),
const SizedBox(height: 4),
Wrap(
spacing: 8,
runSpacing: 6,
children: List.generate(count, (t) => _buildTimePicker(index, t)),
),
if (item.frequency == '每周1次') ...[
const SizedBox(height: 8),
_buildLabel('选择星期'),
const SizedBox(height: 4),
GestureDetector(
onTap: () => _pickWeekday(index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE0E0E0)),
color: const Color(0xFFFAFAFA),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(_weekdays[item.weekday - 1], style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
const SizedBox(width: 4),
const Icon(Icons.keyboard_arrow_down, size: 18, color: Color(0xFF9E9E9E)),
],
),
),
),
],
const SizedBox(height: 8),
],
// Start date
_buildLabel('开始日期'),
const SizedBox(height: 4),
GestureDetector(
onTap: () => _pickDate(index, isStart: true),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE0E0E0)),
color: const Color(0xFFFAFAFA),
),
child: Row(
children: [
Text(
'${item.startDate.year}-${item.startDate.month.toString().padLeft(2, '0')}-${item.startDate.day.toString().padLeft(2, '0')}',
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
),
const Spacer(),
const Icon(Icons.calendar_today, size: 18, color: Color(0xFF9E9E9E)),
],
),
),
),
const SizedBox(height: 8),
// End date (optional)
_buildLabel('结束日期(可选)'),
const SizedBox(height: 4),
GestureDetector(
onTap: () => _pickDate(index, isStart: false),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE0E0E0)),
color: const Color(0xFFFAFAFA),
),
child: Row(
children: [
Text(
item.endDate != null
? '${item.endDate!.year}-${item.endDate!.month.toString().padLeft(2, '0')}-${item.endDate!.day.toString().padLeft(2, '0')}'
: '不设置',
style: TextStyle(
fontSize: 14,
color: item.endDate != null ? const Color(0xFF1A1A1A) : const Color(0xFFBDBDBD),
),
),
const Spacer(),
GestureDetector(
onTap: item.endDate != null ? () => setState(() => item.endDate = null) : null,
child: Icon(
item.endDate != null ? Icons.close : Icons.calendar_today,
size: 18,
color: const Color(0xFF9E9E9E),
),
),
],
),
),
),
],
),
);
}
Widget _buildLabel(String text) {
return Text(
text,
style: const TextStyle(fontSize: 12, color: Color(0xFF757575)),
);
}
Widget _buildTimePicker(int itemIndex, int timeIndex) {
final item = _items[itemIndex];
final time = item.times[timeIndex];
return GestureDetector(
onTap: () => _pickTime(itemIndex, timeIndex),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE0E0E0)),
color: const Color(0xFFFAFAFA),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.access_time, size: 16, color: Color(0xFF635BFF)),
const SizedBox(width: 6),
Text(
time.format(context),
style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)),
),
],
),
),
);
}
Widget _buildAddButton() {
return SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _addItem,
icon: const Icon(Icons.add, size: 18),
label: const Text('添加', style: TextStyle(fontSize: 14)),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF635BFF),
side: const BorderSide(color: Color(0xFFD5D1FF)),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
backgroundColor: const Color(0xFFF5F3FF),
),
),
);
}
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Color(0xFFBDBDBD), fontSize: 14),
filled: true,
fillColor: const Color(0xFFFAFAFA),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF635BFF)),
),
);
}
void _pickFrequency(int index) async {
final selected = await showModalBottomSheet<String>(
context: context,
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: _frequencies
.map((f) => ListTile(
title: Text(f),
onTap: () => Navigator.pop(ctx, f),
))
.toList(),
),
),
);
if (selected != null && mounted) {
setState(() {
final item = _items[index];
item.frequency = selected;
final newCount = _timeCount(selected);
if (newCount > 0 && item.times.length != newCount) {
item.times = List.generate(
newCount,
(i) => TimeOfDay(hour: 8 + i * 4, minute: 0),
);
}
});
}
}
void _pickWeekday(int index) async {
final item = _items[index];
final selected = await showModalBottomSheet<int>(
context: context,
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: List.generate(7, (i) {
return ListTile(
title: Text(_weekdays[i]),
selected: item.weekday == i + 1,
onTap: () => Navigator.pop(ctx, i + 1),
);
}),
),
),
);
if (selected != null && mounted) {
setState(() => _items[index].weekday = selected);
}
}
void _pickTime(int itemIndex, int timeIndex) async {
final item = _items[itemIndex];
final time = await showTimePicker(
context: context,
initialTime: item.times[timeIndex],
);
if (time != null && mounted) {
setState(() => item.times[timeIndex] = time);
}
}
void _pickDate(int index, {required bool isStart}) async {
final item = _items[index];
final initial = isStart ? item.startDate : (item.endDate ?? DateTime.now());
final date = await showDatePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
initialDate: initial,
);
if (date != null && mounted) {
setState(() {
if (isStart) {
item.startDate = date;
} else {
item.endDate = date;
}
});
}
}
}