feat: UI 清新风全面改造 — 朝露主题 + shadcn_ui + 全局字号放大
- 全新"朝露"主题:深青绿主色(#2D6A4F),暖白底,完整设计Token - 引入 shadcn_ui 组件库,MaterialApp 注入 ShadTheme - 新增 4 个公共组件:AppCard、AppMenuItem、AppEmptyState、AppTabChip - 全面消除硬编码颜色,统一使用 AppTheme Token - 全局字号 +3~4pt,图标等比放大 +3px - home/medication/profile/settings/login 等核心页面重写 - 路由和功能逻辑零改动
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
@@ -33,13 +34,13 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FC),
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(title: const Text('服药打卡'), centerTitle: true),
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
if (snap.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7)));
|
||||
return const Center(child: CircularProgressIndicator(color: AppTheme.primary));
|
||||
}
|
||||
final reminders = snap.data ?? [];
|
||||
if (reminders.isEmpty) {
|
||||
@@ -47,7 +48,7 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.check_circle_outline, size: 64, color: Colors.grey[300]),
|
||||
const SizedBox(height: 12),
|
||||
const Text('今天所有用药已打卡', style: TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
||||
const Text('今天所有用药已打卡', style: TextStyle(fontSize: 19, color: Color(0xFF999999))),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -79,19 +80,19 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
|
||||
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(10)),
|
||||
child: const Center(child: Text('💊', style: TextStyle(fontSize: 20))),
|
||||
child: const Center(child: Text('💊', style: TextStyle(fontSize: 23))),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(medName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
|
||||
if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
||||
Text(medName, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700)),
|
||||
if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
|
||||
]),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
@@ -105,12 +106,12 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
child: Row(children: [
|
||||
Icon(
|
||||
isTaken ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||
size: 20,
|
||||
color: isTaken ? const Color(0xFF43A047) : const Color(0xFFCCCCCC),
|
||||
size: 23,
|
||||
color: isTaken ? AppTheme.success : const Color(0xFFCCCCCC),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(time, style: TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w500,
|
||||
fontSize: 18, fontWeight: FontWeight.w500,
|
||||
color: isTaken ? const Color(0xFF999999) : const Color(0xFF333333),
|
||||
)),
|
||||
const Spacer(),
|
||||
@@ -119,12 +120,12 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isTaken ? const Color(0xFFDCFCE7) : const Color(0xFF8B9CF7),
|
||||
color: isTaken ? const Color(0xFFDCFCE7) : AppTheme.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(isTaken ? '已打卡' : '打卡',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600,
|
||||
color: isTaken ? const Color(0xFF43A047) : Colors.white)),
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600,
|
||||
color: isTaken ? AppTheme.success : Colors.white)),
|
||||
),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
@@ -56,8 +57,8 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
try {
|
||||
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) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已保存'), backgroundColor: AppTheme.success)); popRoute(ref); }
|
||||
} catch (_) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: AppTheme.error)); }
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FC),
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(title: Text(widget.id != null ? '编辑用药' : '添加用药')),
|
||||
body: ListView(padding: const EdgeInsets.all(16), children: [
|
||||
// 名称+剂量 一行
|
||||
@@ -93,8 +94,8 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
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))),
|
||||
decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text('${_times[i].hour.toString().padLeft(2,'0')}:${_times[i].minute.toString().padLeft(2,'0')}', style: const TextStyle(fontSize: 18, color: AppTheme.primary, fontWeight: FontWeight.w600))),
|
||||
))),
|
||||
const SizedBox(height: 16),
|
||||
// 开始+结束 一行
|
||||
@@ -106,30 +107,30 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
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)))),
|
||||
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _loading ? null : _save, style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)), child: Text(_loading ? '保存中...' : '保存', style: const TextStyle(fontSize: 19)))),
|
||||
const SizedBox(height: 20),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _label(String text) => Text(text, style: const TextStyle(fontSize: 14, color: Color(0xFF666666)));
|
||||
Widget _label(String text) => Text(text, style: const TextStyle(fontSize: 17, 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)),
|
||||
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: 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 ? 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 _chip(String label, bool sel, VoidCallback onTap) => GestureDetector(onTap: onTap, child: Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: sel ? AppTheme.primary : AppTheme.primaryLight, borderRadius: BorderRadius.circular(16)), child: Text(label, style: TextStyle(fontSize: 16, color: sel ? Colors.white : AppTheme.primary, 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)))),
|
||||
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: 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: 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])),
|
||||
Text(val != null ? '${val.month}/${val.day}' : '不设置', style: TextStyle(fontSize: 18, 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))),
|
||||
if (val != null) GestureDetector(onTap: () => cb(null), child: const Icon(Icons.close, size: 19, color: Color(0xFFBBBBBB))),
|
||||
]))),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
import '../../widgets/app_empty_state.dart';
|
||||
import '../../widgets/app_tab_chip.dart';
|
||||
|
||||
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); }); }
|
||||
|
||||
@@ -18,93 +24,116 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
|
||||
_load();
|
||||
}
|
||||
|
||||
static const _tabs = [('全部', ''), ('服用中', 'active'), ('已停药', 'inactive')];
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FC),
|
||||
appBar: AppBar(title: const Text('用药管理'), actions: [
|
||||
TextButton(onPressed: () { pushRoute(ref, 'medicationEdit'); }, child: const Text('添加', style: TextStyle(fontSize: 15, color: Color(0xFF6C5CE7)))),
|
||||
]),
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('用药管理'),
|
||||
actions: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
child: const Text('添加'),
|
||||
onPressed: () => pushRoute(ref, 'medicationEdit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(children: [
|
||||
Padding(padding: const EdgeInsets.all(12), child: Row(children: [
|
||||
_tab('全部', ''), _tab('服用中', 'active'), _tab('已停药', 'inactive'),
|
||||
])),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.sMd),
|
||||
child: Row(children: _tabs.map((t) => AppTabChip(
|
||||
label: t.$1,
|
||||
selected: _filter == t.$2,
|
||||
onTap: () { _filter = t.$2; _load(); },
|
||||
)).toList()),
|
||||
),
|
||||
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 isActive = m['isActive'] == true;
|
||||
return _SwipeDeleteTile(
|
||||
key: Key(m['id']?.toString() ?? '$i'),
|
||||
onDelete: () => _delete(m['id']?.toString() ?? ''),
|
||||
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': m['id']?.toString() ?? ''}),
|
||||
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))))],
|
||||
if (list.isEmpty) {
|
||||
return AppEmptyState(
|
||||
icon: LucideIcons.pill,
|
||||
title: '暂无用药',
|
||||
subtitle: '点击右上角添加药品',
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, AppTheme.sMd),
|
||||
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 isActive = m['isActive'] == true;
|
||||
final id = m['id']?.toString() ?? '';
|
||||
return Dismissible(
|
||||
key: Key(id),
|
||||
direction: DismissDirection.endToStart,
|
||||
confirmDismiss: (_) async {
|
||||
await _delete(id);
|
||||
return false;
|
||||
},
|
||||
background: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.destructive,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Icon(LucideIcons.trash, color: theme.colorScheme.destructiveForeground, size: 25),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
|
||||
padding: const EdgeInsets.all(AppTheme.sLg),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 44, height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? AppTheme.primaryLight : theme.colorScheme.muted,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
),
|
||||
child: Icon(LucideIcons.pill, size: 25, color: isActive ? AppTheme.primary : theme.colorScheme.mutedForeground),
|
||||
),
|
||||
const SizedBox(width: AppTheme.sMd),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Text(m['name']?.toString() ?? '', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: isActive ? theme.colorScheme.foreground : theme.colorScheme.mutedForeground)),
|
||||
if (!isActive) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.muted,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text('已停', style: TextStyle(fontSize: 13, color: theme.colorScheme.mutedForeground)),
|
||||
),
|
||||
],
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text('${m['dosage'] ?? ''} $times', style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
|
||||
])),
|
||||
Icon(LucideIcons.chevronRight, size: 21, color: theme.colorScheme.border),
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text('${m['dosage'] ?? ''} $times', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
||||
])),
|
||||
const Icon(Icons.chevron_right, size: 20, color: Color(0xFFCCCCCC)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
});
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
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 _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