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:
MingNian
2026-06-10 18:27:38 +08:00
parent 3964cf2bcb
commit 63d2092c24
46 changed files with 2003 additions and 1431 deletions

View File

@@ -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)),
),
),
]),

View File

@@ -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)),
]))),
]);
}

View File

@@ -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),
]),
),
);
},