fix: 用药模块重做 + 运动计划不限7天 + 打卡可切换 + 删除修复
- 用药: 重写列表页(标签筛选+详情弹窗+打卡切换+滑动删除) - 用药: 重写编辑页(选择器替代输入+同列布局+频率改为每天几次) - 用药: 加Notes字段+DELETE端点+修复重复端点 - 运动: 不限7天, startDate+索引推算当天 - 打卡: 药/运动打卡可切换(点✓取消) - 删除: 修复滑动删除热区, 一次点击即删除 - 侧边栏: 一键清理历史对话 - 饮食/运动/用药/问诊 API路径加/api/前缀 - 通知时机修正: 新建计划不再提前弹窗
This commit is contained in:
@@ -3,526 +3,133 @@ 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();
|
||||
final String? id;
|
||||
const MedicationEditPage({super.key, this.id});
|
||||
@override ConsumerState<MedicationEditPage> createState() => _MedicationEditPageState();
|
||||
}
|
||||
|
||||
class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
final _items = <_MedicationItem>[];
|
||||
final _nameCtrls = <TextEditingController>[];
|
||||
final _doseCtrls = <TextEditingController>[];
|
||||
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();
|
||||
_addItem();
|
||||
@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(() {});
|
||||
}
|
||||
|
||||
@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);
|
||||
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 {
|
||||
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': 'Daily',
|
||||
'timeOfDay': timesStr,
|
||||
'startDate': item.startDate.toIso8601String().split('T')[0],
|
||||
if (item.endDate != null)
|
||||
'endDate': item.endDate!.toIso8601String().split('T')[0],
|
||||
'source': 'Manual',
|
||||
});
|
||||
}
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('已添加 ${_items.length} 种药品'),
|
||||
backgroundColor: const Color(0xFF8B9CF7),
|
||||
),
|
||||
);
|
||||
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);
|
||||
}
|
||||
if (widget.id != null) { await srv.update(widget.id!, data); } else { await srv.create(data); }
|
||||
ref.invalidate(medicationListProvider); ref.invalidate(medicationReminderProvider);
|
||||
if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已保存'), backgroundColor: Color(0xFF43A047))); popRoute(ref); }
|
||||
} catch (_) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: Color(0xFFE53935))); }
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FC),
|
||||
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(0xFF8B9CF7),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
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: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(12)),
|
||||
child: Text('${_times[i].hour.toString().padLeft(2,'0')}:${_times[i].minute.toString().padLeft(2,'0')}', style: const TextStyle(fontSize: 15, color: Color(0xFF6C5CE7), 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: const Color(0xFF6C5CE7), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)), child: Text(_loading ? '保存中...' : '保存', style: const TextStyle(fontSize: 16)))),
|
||||
const SizedBox(height: 20),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
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(0xFF8B9CF7),
|
||||
),
|
||||
),
|
||||
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(0xFF8B9CF7)),
|
||||
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(0xFF8B9CF7),
|
||||
side: const BorderSide(color: Color(0xFFD0D5FC)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
backgroundColor: const Color(0xFFF0F2FF),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(0xFF8B9CF7)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Widget _label(String text) => Text(text, style: const TextStyle(fontSize: 14, color: Color(0xFF666666)));
|
||||
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: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)), style: const TextStyle(fontSize: 16)),
|
||||
]);
|
||||
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 ? const Color(0xFF6C5CE7) : const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(16)), child: Text(label, style: TextStyle(fontSize: 13, color: sel ? Colors.white : const Color(0xFF6C5CE7), 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: Colors.white, borderRadius: BorderRadius.circular(12)), child: Text('${val.month}/${val.day}', style: const TextStyle(fontSize: 15)))),
|
||||
]);
|
||||
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: Colors.white, borderRadius: BorderRadius.circular(12)), child: Row(children: [
|
||||
Text(val != null ? '${val.month}/${val.day}' : '不设置', style: TextStyle(fontSize: 15, color: val != null ? null : Colors.grey[400])),
|
||||
if (val != null) const Spacer(),
|
||||
if (val != null) GestureDetector(onTap: () => cb(null), child: const Icon(Icons.close, size: 16, color: Color(0xFFBBBBBB))),
|
||||
]))),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,160 +3,158 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
|
||||
class MedicationListPage extends ConsumerWidget {
|
||||
class MedicationListPage extends ConsumerStatefulWidget {
|
||||
const MedicationListPage({super.key});
|
||||
@override ConsumerState<MedicationListPage> createState() => _MedicationListPageState();
|
||||
}
|
||||
class _MedicationListPageState extends ConsumerState<MedicationListPage> {
|
||||
String _filter = '';
|
||||
Future<List<Map<String, dynamic>>>? _future;
|
||||
@override void initState() { super.initState(); _load(); }
|
||||
void _load() { setState(() { _future = ref.read(medicationServiceProvider).getMedications(_filter); }); }
|
||||
|
||||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||||
final meds = ref.watch(medicationListProvider);
|
||||
Future<void> _checkIn(String id) async {
|
||||
await ref.read(medicationServiceProvider).confirm(id);
|
||||
ref.invalidate(medicationReminderProvider);
|
||||
_load();
|
||||
}
|
||||
Future<void> _delete(String id) async {
|
||||
await ref.read(medicationServiceProvider).deleteMedication(id);
|
||||
_load();
|
||||
}
|
||||
Future<void> _toggleActive(Map<String, dynamic> m) async {
|
||||
await ref.read(medicationServiceProvider).update(m['id']?.toString() ?? '', {'isActive': !(m['isActive'] == true), 'name': m['name'] ?? '', 'dosage': m['dosage'] ?? '', 'frequency': 'Daily'});
|
||||
_load();
|
||||
}
|
||||
|
||||
void _showDetail(Map<String, dynamic> m) {
|
||||
final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join('、') ?? '';
|
||||
final isActive = m['isActive'] == true;
|
||||
showModalBottomSheet(context: context, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))), builder: (ctx) => Padding(padding: const EdgeInsets.all(20), child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Row(children: [
|
||||
Container(width: 48, height: 48, decoration: BoxDecoration(color: isActive ? const Color(0xFFEDEAFF) : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(14)), child: Icon(Icons.medication_outlined, size: 24, color: isActive ? const Color(0xFF6C5CE7) : Colors.grey)),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(child: Text(m['name']?.toString() ?? '', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700))),
|
||||
if (!isActive) const Text(' (已停用)', style: TextStyle(color: Color(0xFF999999))),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
if (m['dosage']?.toString().isNotEmpty == true) _dRow(ctx, '剂量', m['dosage'].toString()),
|
||||
_dRow(ctx, '服用时间', times),
|
||||
_dRow(ctx, '频率', '每天${(m['timeOfDay'] as List?)?.length ?? 0}次'),
|
||||
if (m['startDate'] != null) _dRow(ctx, '开始', m['startDate'].toString().substring(0, 10)),
|
||||
if (m['endDate'] != null) _dRow(ctx, '结束', m['endDate'].toString().substring(0, 10)),
|
||||
if (m['notes']?.toString().isNotEmpty == true) _dRow(ctx, '备注', m['notes'].toString()),
|
||||
const SizedBox(height: 20),
|
||||
Row(children: [
|
||||
Expanded(child: OutlinedButton(onPressed: () { Navigator.pop(ctx); pushRoute(ref, 'medicationEdit', params: {'id': m['id']?.toString() ?? ''}); }, child: const Text('编辑'))),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: OutlinedButton(onPressed: () { Navigator.pop(ctx); _toggleActive(m); }, child: Text(isActive ? '停药' : '恢复服用'), style: OutlinedButton.styleFrom(foregroundColor: isActive ? const Color(0xFFFF9800) : const Color(0xFF43A047)))),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
])));
|
||||
}
|
||||
|
||||
Widget _dRow(BuildContext ctx, String label, String value) => Padding(padding: const EdgeInsets.only(bottom: 8), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(width: 72, child: Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF888888)))),
|
||||
Expanded(child: Text(value, style: const TextStyle(fontSize: 14))),
|
||||
]));
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FC),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
title: const Text('我的用药', style: TextStyle(color: Color(0xFF1A1A1A), fontWeight: FontWeight.w600)),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => pushRoute(ref, 'medicationEdit'),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.add_circle_outline, size: 18, color: Color(0xFF8B9CF7)),
|
||||
const SizedBox(width: 4),
|
||||
const Text('添加新药', style: TextStyle(color: Color(0xFF8B9CF7), fontSize: 14)),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: AppBar(title: const Text('用药管理'), actions: [
|
||||
TextButton(onPressed: () { pushRoute(ref, 'medicationEdit'); }, child: const Text('添加', style: TextStyle(fontSize: 15, color: Color(0xFF6C5CE7)))),
|
||||
]),
|
||||
body: Column(children: [
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row(children: [
|
||||
_TabChip(label: '全部', active: true),
|
||||
const SizedBox(width: 8),
|
||||
_TabChip(label: '服用中'),
|
||||
const SizedBox(width: 8),
|
||||
_TabChip(label: '已停药'),
|
||||
Padding(padding: const EdgeInsets.all(12), child: Row(children: [
|
||||
_tab('全部', ''), _tab('服用中', 'active'), _tab('已停药', 'inactive'),
|
||||
])),
|
||||
Expanded(child: meds.when(
|
||||
data: (list) {
|
||||
if (list.isEmpty) return _empty(context);
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: list.length + 1,
|
||||
itemBuilder: (ctx, i) {
|
||||
if (i == list.length) return const SizedBox(height: 80);
|
||||
final m = list[i];
|
||||
return _MedicationCard(data: m);
|
||||
},
|
||||
);
|
||||
Expanded(child: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
final list = snap.data ?? [];
|
||||
if (list.isEmpty) return Center(child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.medication_outlined, size: 64, color: Colors.grey[300]),
|
||||
const SizedBox(height: 12), Text('暂无用药', style: TextStyle(color: Colors.grey[500])),
|
||||
]));
|
||||
return ListView.builder(padding: const EdgeInsets.fromLTRB(0, 0, 0, 12), itemCount: list.length + 1, itemBuilder: (ctx, i) {
|
||||
if (i == list.length) return const SizedBox(height: 80);
|
||||
final m = list[i];
|
||||
final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(' ') ?? '';
|
||||
final todayTaken = m['todayTaken'] == true;
|
||||
final isActive = m['isActive'] == true;
|
||||
return _SwipeDeleteTile(
|
||||
key: Key(m['id']?.toString() ?? '$i'),
|
||||
onDelete: () => _delete(m['id']?.toString() ?? ''),
|
||||
onTap: () => _showDetail(m),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
||||
child: Row(children: [
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: isActive ? const Color(0xFFEDEAFF) : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(12)), child: Icon(Icons.medication_outlined, size: 22, color: isActive ? const Color(0xFF6C5CE7) : Colors.grey)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Text(m['name']?.toString() ?? '', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: isActive ? const Color(0xFF1A1A1A) : Colors.grey)),
|
||||
if (!isActive) ...[const SizedBox(width: 6), Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration(color: Colors.grey[200], borderRadius: BorderRadius.circular(4)), child: const Text('已停', style: TextStyle(fontSize: 10, color: Color(0xFF999999))))],
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text('${m['dosage'] ?? ''} $times', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
||||
if (m['notes']?.toString().isNotEmpty == true)
|
||||
Text(m['notes'].toString(), style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB)), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
])),
|
||||
if (isActive) GestureDetector(
|
||||
onTap: () => _checkIn(m['id']?.toString() ?? ''),
|
||||
child: Container(width: 40, height: 40,
|
||||
decoration: BoxDecoration(color: todayTaken ? const Color(0xFFDCFCE7) : const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(todayTaken ? Icons.check_circle : Icons.check_circle_outline, size: 24, color: todayTaken ? const Color(0xFF43A047) : const Color(0xFFBBBBBB)),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7))),
|
||||
error: (_, e) => _empty(context),
|
||||
)),
|
||||
_buildReminderBar(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReminderBar() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, boxShadow: [BoxShadow(color: Colors.grey.withAlpha(30), blurRadius: 8)]),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F2FF),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFF8B9CF7).withAlpha(50)),
|
||||
),
|
||||
child: const Icon(Icons.notifications_active_outlined, size: 20, color: Color(0xFF8B9CF7)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('用药提醒已开启', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))),
|
||||
Text('按时服药,守护心脏健康一天', style: TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))),
|
||||
])),
|
||||
const Icon(Icons.chevron_right, size: 18, color: Color(0xFFBDBDBD)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _empty(BuildContext context) {
|
||||
return Center(child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.medication_outlined, size: 64, color: Color(0xFFE0E0E0)),
|
||||
const SizedBox(height: 12),
|
||||
const Text('暂无用药计划', style: TextStyle(fontSize: 15, color: Color(0xFF9E9E9E))),
|
||||
const SizedBox(height: 8),
|
||||
const Text('可通过 AI 对话或手动添加', style: TextStyle(fontSize: 13, color: Color(0xFFBDBDBD))),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
class _TabChip extends StatelessWidget {
|
||||
final String label;
|
||||
final bool active;
|
||||
const _TabChip({required this.label, this.active = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? const Color(0xFF8B9CF7) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: active ? const Color(0xFF8B9CF7) : const Color(0xFFE0E0E0)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: active ? FontWeight.w600 : FontWeight.normal,
|
||||
color: active ? Colors.white : const Color(0xFF757575),
|
||||
),
|
||||
),
|
||||
Widget _tab(String label, String value) {
|
||||
final sel = _filter == value;
|
||||
return GestureDetector(onTap: () { _filter = value; _load(); },
|
||||
child: Container(margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
decoration: BoxDecoration(color: sel ? const Color(0xFF6C5CE7) : const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(16)),
|
||||
child: Text(label, style: TextStyle(fontSize: 13, color: sel ? Colors.white : const Color(0xFF6C5CE7), fontWeight: FontWeight.w500))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MedicationCard extends StatelessWidget {
|
||||
final Map<String, dynamic> data;
|
||||
const _MedicationCard({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))],
|
||||
),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F2FF),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: const Center(child: Text('💊', style: TextStyle(fontSize: 24))),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('${data['name'] ?? ''}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
const SizedBox(height: 4),
|
||||
Text('${data['dosage'] ?? ''} · 每日1次', style: const TextStyle(fontSize: 13, color: Color(0xFF9E9E9E))),
|
||||
const SizedBox(height: 2),
|
||||
Text('08:00 · 剩余 1 片', style: const TextStyle(fontSize: 12, color: Color(0xFFBDBDBD))),
|
||||
])),
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: const BoxDecoration(color: Color(0xFFDCFCE7), shape: BoxShape.circle),
|
||||
child: const Icon(Icons.check, size: 16, color: Color(0xFF43A047)),
|
||||
),
|
||||
]),
|
||||
class _SwipeDeleteTile extends StatefulWidget {
|
||||
final Widget child; final VoidCallback onDelete; final VoidCallback onTap;
|
||||
const _SwipeDeleteTile({super.key, required this.child, required this.onDelete, required this.onTap});
|
||||
@override State<_SwipeDeleteTile> createState() => _SwipeDeleteTileState();
|
||||
}
|
||||
class _SwipeDeleteTileState extends State<_SwipeDeleteTile> with SingleTickerProviderStateMixin {
|
||||
double _dx = 0; static const _max = 80.0, _threshold = 40.0;
|
||||
void _onUpdate(DragUpdateDetails d) => setState(() => _dx = (_dx + d.delta.dx).clamp(-_max, 0.0));
|
||||
void _onEnd(DragEndDetails d) => setState(() => _dx = _dx < -_threshold ? -_max : 0.0);
|
||||
void _doDelete() { widget.onDelete(); setState(() => _dx = 0); }
|
||||
@override Widget build(BuildContext context) {
|
||||
final w = GestureDetector(
|
||||
onHorizontalDragUpdate: _onUpdate,
|
||||
onHorizontalDragEnd: _onEnd,
|
||||
child: Transform.translate(offset: Offset(_dx, 0), child: widget.child),
|
||||
);
|
||||
final red = Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: const BoxDecoration(color: Color(0xFFE53935), borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
child: const Align(alignment: Alignment.centerRight, child: Padding(padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: Colors.white, size: 24))),
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: _dx < 0 ? _doDelete : widget.onTap,
|
||||
child: Stack(children: [Positioned.fill(child: red), w]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user