- 新增管理员角色(手机号12345678910),管理员可增删医生、查看患者 - 注册页重构:去掉角色选择+审核码,改为选医生+填姓名 - 医生端点按DoctorId过滤患者,Patient↔Doctor关系建立 - Doctor/DoctorProfile/User实体新增关联字段 - JSON循环引用修复(IgnoreCycles) - /api/doctors改为公开接口 - 登录闪屏修复+原生Android启动页 - 输入框全局白底+灰框 - 蓝牙重连同步修复 - Web端(doctor_web+health_app/web)全部删除 - 全局UI改版:白底企业风,新配色和组件系统 - 新品牌图标和启动图
399 lines
13 KiB
Dart
399 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../core/app_colors.dart';
|
|
import '../../core/app_theme.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../core/navigation_provider.dart';
|
|
import '../../providers/data_providers.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) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('请输入药品名称')));
|
|
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)
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('保存失败'),
|
|
backgroundColor: AppTheme.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(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),
|
|
Wrap(
|
|
spacing: 8,
|
|
children: List.generate(
|
|
4,
|
|
(i) => _chip(
|
|
'${i + 1}次',
|
|
_timesPerDay == i + 1,
|
|
() => _updateTimes(i + 1),
|
|
),
|
|
),
|
|
),
|
|
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 showTimePicker(
|
|
context: 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: BorderRadius.circular(12),
|
|
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: const TextStyle(
|
|
fontSize: 18,
|
|
color: AppColors.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
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: AppColors.primary,
|
|
side: const BorderSide(color: AppColors.primary, width: 1.5),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
),
|
|
child: Text(
|
|
_loading ? '保存中...' : '保存',
|
|
style: const TextStyle(
|
|
fontSize: 19,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _label(String text) => Text(
|
|
text,
|
|
style: TextStyle(fontSize: 17, color: AppColors.textSecondary),
|
|
);
|
|
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: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
|
borderSide: const BorderSide(color: AppColors.border),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
|
borderSide: const BorderSide(color: AppColors.border),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
|
borderSide: const BorderSide(color: AppColors.primary),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
style: const TextStyle(fontSize: 19),
|
|
),
|
|
],
|
|
);
|
|
Widget _chip(String label, bool sel, VoidCallback onTap) => GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: sel ? Colors.white : AppColors.cardInner,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: sel ? Border.all(color: AppColors.primary, width: 1.5) : null,
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: sel ? AppColors.primary : AppColors.textSecondary,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
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 showDatePicker(
|
|
context: context,
|
|
initialDate: val,
|
|
firstDate: DateTime(2024),
|
|
lastDate: DateTime(2030),
|
|
);
|
|
if (d != null) cb(d);
|
|
},
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.surface,
|
|
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
|
border: Border.all(color: AppColors.border),
|
|
),
|
|
child: Text(
|
|
'${val.month}/${val.day}',
|
|
style: const TextStyle(fontSize: 18),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
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 showDatePicker(
|
|
context: context,
|
|
initialDate: val ?? DateTime.now(),
|
|
firstDate: DateTime(2024),
|
|
lastDate: DateTime(2030),
|
|
);
|
|
if (d != null) cb(d);
|
|
},
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.surface,
|
|
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
|
border: Border.all(color: AppColors.border),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
val != null ? '${val.month}/${val.day}' : '不设置',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: val != null ? null : AppTheme.textHint,
|
|
),
|
|
),
|
|
if (val != null) const Spacer(),
|
|
if (val != null)
|
|
GestureDetector(
|
|
onTap: () => cb(null),
|
|
child: const Icon(
|
|
Icons.close,
|
|
size: 19,
|
|
color: AppColors.textHint,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|