feat: UI全面改造 + 后端多项修复
【后端修复】 - 删除diet/consultation agent假工具声明 - 修复DayOfWeek实体注释 - 修复Vision API content序列化 - cleanup_service级联删除修复 - 用药提醒时区偏差修复 - 统一DateTime处理(UtcNow+8) - 新增UTC DateTime JSON转换器 【前端UI重构】 - 配色体系全面更新(#8B5CF6淡紫+#F0ECFF背景) - 登录页重设计 - 首页重设计(透明顶栏、渐变背景、胶囊输入区) - 聊天卡片加白蓝边框、渐变标题 - 侧边栏重构(渐变背景、合并顶部、删除底部设置) - 确认卡片可编辑字段恢复 - 所有子页面加返回按钮 - catch异常加日志 - 删除后refresh provider缓存
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
@@ -29,13 +31,13 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
ref.invalidate(medicationReminderProvider);
|
||||
ref.invalidate(medicationListProvider);
|
||||
_load();
|
||||
} catch (_) {}
|
||||
} catch (e) { debugPrint('[MedCheckIn] 打卡失败: $e'); }
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(title: const Text('服药打卡'), centerTitle: true),
|
||||
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('服药打卡'), centerTitle: true),
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
@@ -46,9 +48,9 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
if (reminders.isEmpty) {
|
||||
return Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.check_circle_outline, size: 64, color: Colors.grey[300]),
|
||||
Icon(Icons.check_circle_outline, size: 64, color: AppTheme.textHint),
|
||||
const SizedBox(height: 12),
|
||||
const Text('今天所有用药已打卡', style: TextStyle(fontSize: 19, color: Color(0xFF999999))),
|
||||
const Text('今天所有用药已打卡', style: TextStyle(fontSize: 19, color: AppColors.textSecondary)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -78,21 +80,21 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
boxShadow: [AppTheme.shadowCard],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(10)),
|
||||
decoration: BoxDecoration(color: AppColors.warningLight, borderRadius: BorderRadius.circular(10)),
|
||||
child: const Center(child: Text('💊', style: TextStyle(fontSize: 23))),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(medName, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700)),
|
||||
if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
|
||||
if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
||||
]),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
@@ -107,12 +109,12 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
Icon(
|
||||
isTaken ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||
size: 23,
|
||||
color: isTaken ? AppTheme.success : const Color(0xFFCCCCCC),
|
||||
color: isTaken ? AppTheme.success : AppTheme.border,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(time, style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w500,
|
||||
color: isTaken ? const Color(0xFF999999) : const Color(0xFF333333),
|
||||
color: isTaken ? AppTheme.textSub : AppTheme.text,
|
||||
)),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
@@ -120,12 +122,12 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isTaken ? const Color(0xFFDCFCE7) : AppTheme.primary,
|
||||
color: isTaken ? AppColors.successLight : AppTheme.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(isTaken ? '已打卡' : '打卡',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600,
|
||||
color: isTaken ? AppTheme.success : Colors.white)),
|
||||
color: isTaken ? AppTheme.success : AppColors.textOnGradient)),
|
||||
),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -57,7 +58,7 @@ 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: AppTheme.success)); popRoute(ref); }
|
||||
if (mounted) { popRoute(ref); }
|
||||
} catch (_) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: AppTheme.error)); }
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
@@ -113,24 +114,24 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _label(String text) => Text(text, style: const TextStyle(fontSize: 17, color: Color(0xFF666666)));
|
||||
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: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)), style: const TextStyle(fontSize: 19)),
|
||||
TextField(controller: ctrl, decoration: InputDecoration(hintText: hint, filled: true, fillColor: AppTheme.surface, border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppTheme.rMd), 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 ? 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: 18)))),
|
||||
child: Container(width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rMd)), 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: 18, color: val != null ? null : Colors.grey[400])),
|
||||
child: Container(width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rMd)), 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: Color(0xFFBBBBBB))),
|
||||
if (val != null) GestureDetector(onTap: () => cb(null), child: const Icon(Icons.close, size: 19, color: AppColors.textHint)),
|
||||
]))),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../core/navigation_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
import '../../widgets/app_empty_state.dart';
|
||||
import '../../widgets/app_tab_chip.dart';
|
||||
import '../../widgets/common_widgets.dart';
|
||||
|
||||
class MedicationListPage extends ConsumerStatefulWidget {
|
||||
const MedicationListPage({super.key});
|
||||
@@ -21,16 +22,17 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
|
||||
|
||||
Future<void> _delete(String id) async {
|
||||
await ref.read(medicationServiceProvider).deleteMedication(id);
|
||||
ref.invalidate(medicationListProvider);
|
||||
_load();
|
||||
}
|
||||
|
||||
static const _tabs = [('全部', ''), ('服用中', 'active'), ('已停药', 'inactive')];
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)),
|
||||
title: const Text('用药管理'),
|
||||
actions: [
|
||||
ShadButton.ghost(
|
||||
@@ -69,64 +71,48 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
|
||||
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(
|
||||
return SwipeDeleteTile(
|
||||
key: Key(id),
|
||||
direction: DismissDirection.endToStart,
|
||||
confirmDismiss: (_) async {
|
||||
await _delete(id);
|
||||
return false;
|
||||
},
|
||||
background: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
|
||||
onDelete: () => _delete(id),
|
||||
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}),
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppTheme.sLg),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.destructive,
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
),
|
||||
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),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 44, height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? AppTheme.primaryLight : AppTheme.bgSoft,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
),
|
||||
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)),
|
||||
child: Icon(LucideIcons.pill, size: 25, color: isActive ? AppTheme.primary : AppTheme.textSub),
|
||||
),
|
||||
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 ? AppTheme.text : AppTheme.textSub)),
|
||||
if (!isActive) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.bgSoft,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
],
|
||||
]),
|
||||
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),
|
||||
]),
|
||||
),
|
||||
child: Text('已停', style: TextStyle(fontSize: 13, color: AppTheme.textSub)),
|
||||
),
|
||||
],
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text('${m['dosage'] ?? ''} $times', style: TextStyle(fontSize: 16, color: AppTheme.textSub)),
|
||||
])),
|
||||
Icon(LucideIcons.chevronRight, size: 21, color: AppTheme.border),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user