fix: 健康仪表盘单位下移 + 档案按钮浅紫 + AI问诊胶囊改名 + 删除账号功能

This commit is contained in:
MingNian
2026-06-17 17:35:47 +08:00
parent 0f2a9c1c1a
commit fbaed0cf1d
11 changed files with 1289 additions and 1500 deletions

1
PC Normal file
View File

@@ -0,0 +1 @@
Phone - forwarding OK

View File

@@ -73,8 +73,33 @@ public static class UserEndpoints
group.MapDelete("/account", async (HttpContext http, AppDbContext db, CancellationToken ct) => group.MapDelete("/account", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
{ {
var userId = GetUserId(http); var userId = GetUserId(http);
var user = await db.Users.FindAsync([userId], ct); // 医生清除Doctor关联
if (user != null) { db.Users.Remove(user); await db.SaveChangesAsync(ct); } var profile = await db.DoctorProfiles.FirstOrDefaultAsync(p => p.UserId == userId, ct);
if (profile?.DoctorId != null)
{
await db.Users.Where(u => u.DoctorId == profile!.DoctorId).ExecuteUpdateAsync(u => u.SetProperty(x => x.DoctorId, (Guid?)null), ct);
await db.Doctors.Where(d => d.Id == profile.DoctorId).ExecuteDeleteAsync(ct);
}
// 删除所有关联数据
await db.HealthRecords.Where(r => r.UserId == userId).ExecuteDeleteAsync(ct);
await db.MedicationLogs.Where(l => l.UserId == userId).ExecuteDeleteAsync(ct);
await db.Medications.Where(m => m.UserId == userId).ExecuteDeleteAsync(ct);
await db.DietFoodItems.Where(i => i.DietRecord!.UserId == userId).ExecuteDeleteAsync(ct);
await db.DietRecords.Where(d => d.UserId == userId).ExecuteDeleteAsync(ct);
await db.ExercisePlanItems.Where(i => i.Plan!.UserId == userId).ExecuteDeleteAsync(ct);
await db.ExercisePlans.Where(p => p.UserId == userId).ExecuteDeleteAsync(ct);
await db.Reports.Where(r => r.UserId == userId).ExecuteDeleteAsync(ct);
await db.ConversationMessages.Where(m => m.Conversation!.UserId == userId).ExecuteDeleteAsync(ct);
await db.Conversations.Where(c => c.UserId == userId).ExecuteDeleteAsync(ct);
await db.ConsultationMessages.Where(m => m.Consultation!.UserId == userId).ExecuteDeleteAsync(ct);
await db.Consultations.Where(c => c.UserId == userId).ExecuteDeleteAsync(ct);
await db.FollowUps.Where(f => f.UserId == userId).ExecuteDeleteAsync(ct);
await db.RefreshTokens.Where(t => t.UserId == userId).ExecuteDeleteAsync(ct);
await db.DeviceTokens.Where(t => t.UserId == userId).ExecuteDeleteAsync(ct);
await db.NotificationPreferences.Where(p => p.UserId == userId).ExecuteDeleteAsync(ct);
await db.HealthArchives.Where(a => a.UserId == userId).ExecuteDeleteAsync(ct);
await db.DoctorProfiles.Where(p => p.UserId == userId).ExecuteDeleteAsync(ct);
await db.Users.Where(u => u.Id == userId).ExecuteDeleteAsync(ct);
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null }); return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
}); });
} }

View File

@@ -13,7 +13,6 @@ import '../pages/consultation/consultation_pages.dart';
import '../pages/settings/settings_pages.dart'; import '../pages/settings/settings_pages.dart';
import '../pages/settings/notification_prefs_page.dart'; import '../pages/settings/notification_prefs_page.dart';
import '../pages/profile/profile_page.dart'; import '../pages/profile/profile_page.dart';
import '../pages/profile/service_package_detail_page.dart';
import '../pages/diet/diet_capture_page.dart'; import '../pages/diet/diet_capture_page.dart';
import '../pages/device/device_scan_page.dart'; import '../pages/device/device_scan_page.dart';
import '../pages/device/device_management_page.dart'; import '../pages/device/device_management_page.dart';
@@ -32,45 +31,74 @@ import '../providers/auth_provider.dart' show userRoleProvider;
Widget buildPage(RouteInfo route, WidgetRef ref) { Widget buildPage(RouteInfo route, WidgetRef ref) {
final params = route.params; final params = route.params;
switch (route.name) { switch (route.name) {
case 'login': return const LoginPage(); case 'login':
return const LoginPage();
case 'home': case 'home':
final role = ref.watch(userRoleProvider); final role = ref.watch(userRoleProvider);
return role == 'Doctor' ? const DoctorHomePage() : const HomePage(); return role == 'Doctor' ? const DoctorHomePage() : const HomePage();
case 'doctorHome': return const DoctorHomePage(); case 'doctorHome':
case 'adminHome': return const AdminHomePage(); return const DoctorHomePage();
case 'adminAddDoctor': return const AdminAddDoctorPage(); case 'adminHome':
case 'trend': return TrendPage(metricType: params['type']?.isNotEmpty == true ? params['type'] : null); return const AdminHomePage();
case 'calendar': return const HealthCalendarPage(); case 'adminAddDoctor':
case 'medications': return const MedicationListPage(); return const AdminAddDoctorPage();
case 'medCheckIn': return MedicationCheckInPage(medId: params['id']); case 'trend':
case 'medicationEdit': return const MedicationEditPage(); return TrendPage(
case 'reports': return const ReportListPage(); metricType: params['type']?.isNotEmpty == true ? params['type'] : null,
case 'aiAnalysis': return AiAnalysisPage(id: params['id']!); );
case 'consultation': return DoctorChatPage(id: params['id']!); case 'calendar':
case 'exercisePlan': return const ExercisePlanPage(); return const HealthCalendarPage();
case 'exerciseCreate': return const ExercisePlanCreatePage(); case 'medications':
case 'dietRecords': return const DietRecordListPage(); return const MedicationListPage();
case 'dietCapture': return const DietCapturePage(); case 'medCheckIn':
case 'profile': return const ProfilePage(); return MedicationCheckInPage(medId: params['id']);
case 'doctorProfile': return const DoctorProfileEditPage(); case 'medicationEdit':
case 'doctorSettings': return const DoctorSettingsPage(); return const MedicationEditPage();
case 'doctorPatientDetail': return DoctorPatientDetailPage(id: params['id']!); case 'reports':
case 'doctorChat': return DoctorChatPage(id: params['id']!); return const ReportListPage();
case 'doctorReportDetail': return DoctorReportDetailPage(id: params['id']!); case 'aiAnalysis':
case 'doctorFollowUpEdit': return DoctorFollowUpEditPage(id: params['id']!); return AiAnalysisPage(id: params['id']!);
case 'devices': return const DeviceManagementPage(); case 'exercisePlan':
case 'deviceScan': return const DeviceScanPage(); return const ExercisePlanPage();
case 'healthArchive': return const HealthArchivePage(); case 'exerciseCreate':
case 'followups': return const FollowUpListPage(); return const ExercisePlanCreatePage();
case 'settings': return const SettingsPage(); case 'dietRecords':
case 'notificationPrefs': return const NotificationPrefsPage(); return const DietRecordListPage();
case 'staticText': return StaticTextPage(type: params['type']!); case 'dietCapture':
case 'servicePackageDetail': return ServicePackageDetailPage(packageId: params['id']!); return const DietCapturePage();
case 'dietDetail': return DietRecordDetailPage(id: params['id']!); case 'profile':
default: return const LoginPage(); return const ProfilePage();
case 'doctorProfile':
return const DoctorProfileEditPage();
case 'doctorSettings':
return const DoctorSettingsPage();
case 'doctorPatientDetail':
return DoctorPatientDetailPage(id: params['id']!);
case 'doctorChat':
return DoctorChatPage(id: params['id']!);
case 'doctorReportDetail':
return DoctorReportDetailPage(id: params['id']!);
case 'doctorFollowUpEdit':
return DoctorFollowUpEditPage(id: params['id']!);
case 'devices':
return const DeviceManagementPage();
case 'deviceScan':
return const DeviceScanPage();
case 'healthArchive':
return const HealthArchivePage();
case 'followups':
return const FollowUpListPage();
case 'settings':
return const SettingsPage();
case 'notificationPrefs':
return const NotificationPrefsPage();
case 'staticText':
return StaticTextPage(type: params['type']!);
case 'dietDetail':
return DietRecordDetailPage(id: params['id']!);
default:
return const LoginPage();
} }
} }
// ── 医生端次级页面 Stubs逐步实现── // ── 医生端次级页面 Stubs逐步实现──

View File

@@ -302,8 +302,14 @@ class DietNotifier extends Notifier<DietState> {
} }
// ─────────── 饮食主题色(暖橙系,不再用紫色)─────────── // ─────────── 饮食主题色(暖橙系,不再用紫色)───────────
const _dietAccent = AppColors.primary; const _dietAccent = Color(0xFFF97316);
const _dietAccentLight = AppColors.primarySoft; const _dietAccentLight = Color(0xFFFFF3E0);
const _dietKcalText = Color(0xFF9A3412);
const _dietGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFF6D365), Color(0xFFFDA085)],
);
class DietCapturePage extends ConsumerStatefulWidget { class DietCapturePage extends ConsumerStatefulWidget {
const DietCapturePage({super.key}); const DietCapturePage({super.key});
@@ -404,11 +410,11 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
('🍪', '加餐', 'snack'), ('🍪', '加餐', 'snack'),
]; ];
return Container( return Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(6),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: AppColors.surfaceGradient, color: Colors.white.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.borderLight), border: Border.all(color: Colors.white, width: 1.4),
boxShadow: AppColors.cardShadowLight, boxShadow: AppColors.cardShadowLight,
), ),
child: Row( child: Row(
@@ -419,23 +425,28 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
onTap: () => ref.read(dietProvider.notifier).setMealType(m.$3), onTap: () => ref.read(dietProvider.notifier).setMealType(m.$3),
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 12), curve: Curves.easeOutCubic,
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(vertical: 11),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: sel ? AppColors.warmCareGradient : null, gradient: sel ? _dietGradient : null,
color: sel ? null : Colors.white, color: sel ? null : const Color(0xFFFFFBF6),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(16),
border: Border.all(
color: sel ? Colors.transparent : const Color(0xFFFFE4CA),
),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(m.$1, style: const TextStyle(fontSize: 22)), const SizedBox.shrink(),
const SizedBox(height: 2), const SizedBox(height: 0),
Text( Text(
m.$2, m.$2,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w900,
color: sel ? Colors.white : AppColors.textHint, color: sel ? Colors.white : _dietKcalText,
), ),
), ),
], ],
@@ -523,7 +534,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
vertical: 4, vertical: 4,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.warningLight, color: const Color(0xFFFFF3D8),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
@@ -531,7 +542,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.warning, color: _dietKcalText,
), ),
), ),
), ),
@@ -645,7 +656,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.warning, color: _dietKcalText,
), ),
align: TextAlign.right, align: TextAlign.right,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -656,7 +667,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
'kcal', 'kcal',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppColors.textHint, color: Color(0xFFB45309),
), ),
), ),
], ],
@@ -715,7 +726,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primary), borderSide: const BorderSide(color: _dietAccent),
), ),
hintText: hint, hintText: hint,
hintStyle: const TextStyle(fontSize: 14, color: AppColors.textHint), hintStyle: const TextStyle(fontSize: 14, color: AppColors.textHint),
@@ -737,62 +748,62 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
border: Border.all(color: AppColors.border), border: Border.all(color: AppColors.border),
boxShadow: AppColors.cardShadowLight, boxShadow: AppColors.cardShadowLight,
), ),
child: Row( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( const Text(
width: 56, '本餐热量',
height: 56, style: TextStyle(
child: Stack( fontSize: 16,
alignment: Alignment.center, fontWeight: FontWeight.w800,
children: [ color: AppColors.textPrimary,
SizedBox( ),
width: 56, ),
height: 56, const SizedBox(height: 14),
child: CircularProgressIndicator( Row(
value: (totalCal / 700).clamp(0.0, 1.0), children: [
strokeWidth: 4, SizedBox(
backgroundColor: AppColors.borderLight, width: 56,
color: _dietAccent, height: 56,
), child: Stack(
), alignment: Alignment.center,
Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( SizedBox(
'$totalCal', width: 56,
style: const TextStyle( height: 56,
fontSize: 16, child: CircularProgressIndicator(
fontWeight: FontWeight.w800, value: (totalCal / 700).clamp(0.0, 1.0),
strokeWidth: 4,
backgroundColor: AppColors.borderLight,
color: _dietAccent, color: _dietAccent,
), ),
), ),
const Text( Column(
'kcal', mainAxisSize: MainAxisSize.min,
style: TextStyle( children: [
fontSize: 12, Text(
color: AppColors.textSecondary, '$totalCal',
), style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: _dietAccent,
),
),
const Text(
'kcal',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
), ),
], ],
), ),
], ),
), const SizedBox(width: 16),
), Expanded(
const SizedBox(width: 16), child: Row(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'本餐热量',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 6),
Row(
children: [ children: [
_macro('碳水', 0.55, const Color(0xFFF5A623)), _macro('碳水', 0.55, const Color(0xFFF5A623)),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -801,8 +812,8 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
_macro('脂肪', 0.20, const Color(0xFFE8686A)), _macro('脂肪', 0.20, const Color(0xFFE8686A)),
], ],
), ),
], ),
), ],
), ),
], ],
), ),
@@ -855,28 +866,46 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.border), border: Border.all(color: AppColors.border),
), ),
child: Row( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( const Text(
width: 36, 'AI建议',
height: 36, style: TextStyle(
decoration: BoxDecoration( fontSize: 16,
color: _dietAccentLight, fontWeight: FontWeight.w800,
borderRadius: BorderRadius.circular(10), color: AppColors.textPrimary,
), ),
child: const Icon(Icons.auto_awesome, size: 20, color: _dietAccent),
), ),
const SizedBox(width: 12), const SizedBox(height: 12),
Expanded( Row(
child: Text( crossAxisAlignment: CrossAxisAlignment.start,
text, children: [
style: const TextStyle( Container(
fontSize: 16, width: 36,
color: AppColors.textPrimary, height: 36,
height: 1.6, decoration: BoxDecoration(
color: _dietAccentLight,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.auto_awesome,
size: 20,
color: _dietAccent,
),
), ),
), const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 16,
color: AppColors.textPrimary,
height: 1.6,
),
),
),
],
), ),
], ],
), ),
@@ -889,9 +918,15 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
width: double.infinity, width: double.infinity,
height: 52, height: 52,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: AppColors.warmCareGradient, gradient: _dietGradient,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: AppColors.buttonShadow, boxShadow: [
BoxShadow(
color: _dietAccent.withValues(alpha: 0.24),
blurRadius: 16,
offset: const Offset(0, 8),
),
],
), ),
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,

View File

@@ -176,7 +176,7 @@ class _HomePageState extends ConsumerState<HomePage> {
} }
static final _agentDefs = [ static final _agentDefs = [
(ActiveAgent.consultation, '问诊', LucideIcons.messageCircle), (ActiveAgent.consultation, 'AI问诊', LucideIcons.messageCircle),
(ActiveAgent.health, '记数据', LucideIcons.heartPulse), (ActiveAgent.health, '记数据', LucideIcons.heartPulse),
(ActiveAgent.diet, '拍饮食', LucideIcons.utensils), (ActiveAgent.diet, '拍饮食', LucideIcons.utensils),
(ActiveAgent.medication, '药管家', LucideIcons.pill), (ActiveAgent.medication, '药管家', LucideIcons.pill),
@@ -244,12 +244,36 @@ class _HomePageState extends ConsumerState<HomePage> {
LinearGradient _agentGradient(ActiveAgent agent) { LinearGradient _agentGradient(ActiveAgent agent) {
return switch (agent) { return switch (agent) {
ActiveAgent.consultation => AppColors.sageVioletGradient, ActiveAgent.consultation => const LinearGradient(
ActiveAgent.health => AppColors.meadowVioletGradient, begin: Alignment.topCenter,
ActiveAgent.diet => AppColors.warmCareGradient, end: Alignment.bottomCenter,
ActiveAgent.medication => AppColors.primaryGradient, colors: [Color(0xFFFFCAD4), Color(0xFFFF9AAE)],
ActiveAgent.report => AppColors.roseVioletGradient, ),
ActiveAgent.exercise => AppColors.calmHealthGradient, ActiveAgent.health => const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFFAFBD), Color(0xFFC9FFBF)],
),
ActiveAgent.diet => const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFF6D365), Color(0xFFFDA085)],
),
ActiveAgent.medication => const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF89F7FE), Color(0xFF66A6FF)],
),
ActiveAgent.report => const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFE0C3FC), Color(0xFF8EC5FC)],
),
ActiveAgent.exercise => const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFB6F2FF), Color(0xFF7DD3FC)],
),
_ => AppColors.primaryGradient, _ => AppColors.primaryGradient,
}; };
} }

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../core/app_colors.dart'; import '../../../core/app_colors.dart';
import '../../../core/app_theme.dart'; import '../../../core/app_theme.dart';
import '../../../core/navigation_provider.dart'; import '../../../core/navigation_provider.dart';
@@ -294,12 +295,27 @@ class ChatMessagesView extends ConsumerWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
// ── 医生选择区(问诊专用)── // ── AI问诊引导 ──
if (agent == ActiveAgent.consultation) ...[ if (agent == ActiveAgent.consultation) ...[
const SizedBox(height: 14), const SizedBox(height: 14),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: _buildDoctorCards(ref, agentColors), child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF0F4FF),
borderRadius: BorderRadius.circular(14),
),
child: const Text(
'在下方输入框描述您的症状AI医生将为您提供专业建议',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
),
),
),
), ),
], ],
@@ -409,13 +425,18 @@ class ChatMessagesView extends ConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ShaderMask( ShaderMask(
shaderCallback: (bounds) => AppColors.actionOutlineGradient.createShader(bounds), shaderCallback: (bounds) =>
AppColors.actionOutlineGradient.createShader(bounds),
child: Icon(a.icon, size: 24, color: Colors.white), child: Icon(a.icon, size: 24, color: Colors.white),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
a.label, a.label,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: AppColors.textPrimary), style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
), ),
], ],
), ),
@@ -436,113 +457,6 @@ class ChatMessagesView extends ConsumerWidget {
}; };
} }
Widget _buildDoctorCards(WidgetRef ref, _AgentColors colors) {
const doctors = [
{
'name': '张明',
'title': '主任医师',
'dept': '心脏康复科',
'desc': '冠心病、高血压术后管理',
'id': '468b82e2-d95a-4436-bff6-a50eecf99a66',
},
{
'name': '李芳',
'title': '副主任医师',
'dept': '营养科',
'desc': '糖尿病、甲状腺疾病管理',
'id': 'd4148733-b538-4398-af17-0c7592fc0c2d',
},
{
'name': '王建国',
'title': '主任医师',
'dept': '心血管内科',
'desc': '术后营养指导、饮食方案制定',
'id': 'ef0953c9-eb63-4d03-b6d7-050a1897d4a3',
},
];
return Row(
children: [
for (var i = 0; i < doctors.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
Expanded(child: _doctorCard(doctors[i], ref, colors)),
],
],
);
}
Widget _doctorCard(
Map<String, String> doc,
WidgetRef ref,
_AgentColors colors,
) {
return InkWell(
onTap: () => pushRoute(ref, 'consultation', params: {'id': doc['id']!}),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.border),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 22,
backgroundColor: colors.iconBg,
child: Text(
doc['name']![0],
style: TextStyle(
fontSize: 21,
fontWeight: FontWeight.w600,
color: colors.accent,
),
),
),
const SizedBox(height: 6),
Text(
doc['name']!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
Text(
doc['title']!,
style: const TextStyle(fontSize: 14, color: AppColors.textHint),
),
const SizedBox(height: 2),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: colors.iconBg,
borderRadius: BorderRadius.circular(4),
),
child: Text(
doc['dept']!,
style: TextStyle(fontSize: 13, color: colors.accent),
),
),
const SizedBox(height: 4),
Text(
doc['desc']!,
style: const TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
height: 1.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
);
}
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 2. DataConfirmCard — 统一确认卡片(紫白蓝风格) // 2. DataConfirmCard — 统一确认卡片(紫白蓝风格)
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
@@ -1556,46 +1470,52 @@ class ChatMessagesView extends ConsumerWidget {
static _AgentColors _agentColors(ActiveAgent agent) { static _AgentColors _agentColors(ActiveAgent agent) {
return switch (agent) { return switch (agent) {
ActiveAgent.consultation => _AgentColors( ActiveAgent.consultation => _AgentColors(
gradient: [AppColors.sageAccent, AppColors.primary], gradient: [Color(0xFFFFCAD4), Color(0xFFFF9AAE)],
bg: const Color(0xFFF2FBF9), bg: const Color(0xFFFFF4F6),
border: const Color(0xFFD9F0EA), border: const Color(0xFFFFDFE7),
iconBg: const Color(0xFFE8F8F4), iconBg: const Color(0xFFFFEEF2),
accent: const Color(0xFF4F9E90), accent: const Color(0xFFFF7F98),
verticalGradient: true,
), ),
ActiveAgent.health => _AgentColors( ActiveAgent.health => _AgentColors(
gradient: [AppColors.meadowAccent, AppColors.auraDeepIndigo], gradient: [Color(0xFFFFAFBD), Color(0xFFC9FFBF)],
bg: const Color(0xFFF3FBF4), bg: const Color(0xFFF7FDEB),
border: const Color(0xFFDFF2E4), border: const Color(0xFFEAF7BF),
iconBg: const Color(0xFFEFF9F0), iconBg: const Color(0xFFF9FCEB),
accent: const Color(0xFF4EA65B), accent: const Color(0xFF8BD982),
verticalGradient: true,
), ),
ActiveAgent.diet => _AgentColors( ActiveAgent.diet => _AgentColors(
gradient: [AppColors.peachAccent, AppColors.auraOrchid], gradient: [Color(0xFFF6D365), Color(0xFFFDA085)],
bg: const Color(0xFFFFF7F0), bg: const Color(0xFFFFF7F0),
border: const Color(0xFFFFE8D4), border: const Color(0xFFFFE8D4),
iconBg: const Color(0xFFFFF0E4), iconBg: const Color(0xFFFFF0E4),
accent: const Color(0xFFE26A64), accent: const Color(0xFFF89C5B),
verticalGradient: true,
), ),
ActiveAgent.medication => _AgentColors( ActiveAgent.medication => _AgentColors(
gradient: [AppColors.auraLavender, AppColors.blueMeasure], gradient: [Color(0xFF89F7FE), Color(0xFF66A6FF)],
bg: const Color(0xFFF4F6FF), bg: const Color(0xFFF4F6FF),
border: const Color(0xFFDDE6FF), border: const Color(0xFFDDE6FF),
iconBg: const Color(0xFFEFF3FF), iconBg: const Color(0xFFEFF3FF),
accent: AppColors.primary, accent: const Color(0xFF4F8FF7),
verticalGradient: true,
), ),
ActiveAgent.report => _AgentColors( ActiveAgent.report => _AgentColors(
gradient: [AppColors.primary, AppColors.roseAccent], gradient: [Color(0xFFE0C3FC), Color(0xFF8EC5FC)],
bg: const Color(0xFFF8F5FF), bg: const Color(0xFFF8F5FF),
border: const Color(0xFFE9E0FF), border: const Color(0xFFE9E0FF),
iconBg: const Color(0xFFF2EDFF), iconBg: const Color(0xFFF2EDFF),
accent: AppColors.primary, accent: const Color(0xFF9D8AF8),
verticalGradient: true,
), ),
ActiveAgent.exercise => _AgentColors( ActiveAgent.exercise => _AgentColors(
gradient: [AppColors.sageAccent, AppColors.auraDeepIndigo], gradient: [Color(0xFFB6F2FF), Color(0xFF7DD3FC)],
bg: const Color(0xFFF0FBFD), bg: const Color(0xFFF0FCFF),
border: const Color(0xFFD6EEF2), border: const Color(0xFFD8F5FC),
iconBg: const Color(0xFFE8F7FA), iconBg: const Color(0xFFEAFBFF),
accent: const Color(0xFF2B8EA0), accent: const Color(0xFF38BDE8),
verticalGradient: true,
), ),
_ => _AgentColors( _ => _AgentColors(
gradient: [AppColors.primary, AppColors.blueMeasure], gradient: [AppColors.primary, AppColors.blueMeasure],
@@ -1609,16 +1529,16 @@ class ChatMessagesView extends ConsumerWidget {
static (_AgentIcon, String, String) _agentInfo(ActiveAgent agent) { static (_AgentIcon, String, String) _agentInfo(ActiveAgent agent) {
return switch (agent) { return switch (agent) {
ActiveAgent.health => (Icons.favorite_border, '记数据', '录入血压、血糖、心率等日常指标'), ActiveAgent.health => (LucideIcons.heartPulse, '记数据', '录入血压、血糖、心率等日常指标'),
ActiveAgent.diet => (Icons.restaurant, '拍饮食', '拍照识别食物热量和营养成分'), ActiveAgent.diet => (LucideIcons.utensils, '拍饮食', '拍照识别食物热量和营养成分'),
ActiveAgent.medication => (Icons.medication, '药管家', '管理药品、提醒服药、追踪用量'), ActiveAgent.medication => (LucideIcons.pill, '药管家', '管理药品、提醒服药、追踪用量'),
ActiveAgent.consultation => ( ActiveAgent.consultation => (
Icons.local_hospital, LucideIcons.messageCircle,
'问诊', 'AI问诊',
'在线咨询医生,描述症状获取建议', 'AI智能问诊,描述症状获取建议',
), ),
ActiveAgent.report => (Icons.assignment, '报告分析', '上传体检报告AI 辅助解读'), ActiveAgent.report => (LucideIcons.fileText, '报告分析', '上传体检报告AI 辅助解读'),
ActiveAgent.exercise => (Icons.directions_run, '运动', '制定运动计划,打卡记录进度'), ActiveAgent.exercise => (LucideIcons.activity, '运动', '制定运动计划,打卡记录进度'),
_ => (Icons.forum_outlined, 'AI 助手', '您的智能健康管家'), _ => (Icons.forum_outlined, 'AI 助手', '您的智能健康管家'),
}; };
} }
@@ -2031,13 +1951,17 @@ class _AgentMark extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: colors.gradient, colors: colors.gradient,
begin: Alignment.topLeft, begin: colors.verticalGradient
end: Alignment.bottomRight, ? Alignment.topCenter
: Alignment.topLeft,
end: colors.verticalGradient
? Alignment.bottomCenter
: Alignment.bottomRight,
), ),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(colors.verticalGradient ? 18 : 20),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: colors.accent.withValues(alpha: 0.24), color: colors.accent.withValues(alpha: 0.18),
blurRadius: 18, blurRadius: 18,
offset: const Offset(0, 8), offset: const Offset(0, 8),
), ),
@@ -2103,12 +2027,14 @@ class _AgentColors {
final Color border; final Color border;
final Color iconBg; final Color iconBg;
final Color accent; final Color accent;
final bool verticalGradient;
const _AgentColors({ const _AgentColors({
required this.gradient, required this.gradient,
required this.bg, required this.bg,
required this.border, required this.border,
required this.iconBg, required this.iconBg,
required this.accent, required this.accent,
this.verticalGradient = false,
}); });
} }

View File

@@ -17,127 +17,85 @@ class ProfilePage extends ConsumerWidget {
return GradientScaffold( return GradientScaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white.withValues(alpha: 0.9), backgroundColor: Colors.white.withValues(alpha: 0.86),
title: const Text('个人信息'), leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 19),
onPressed: () => popRoute(ref),
),
title: const Text(
'个人信息',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800),
),
centerTitle: true,
), ),
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.fromLTRB(20, 18, 20, 34),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 20), _ProfileHero(
name: name,
// 头像区 phone: phone,
Center( avatarUrl: user?.avatarUrl,
child: CircleAvatar(
radius: 50,
backgroundColor: AppColors.primaryLight,
backgroundImage: user?.avatarUrl != null
? NetworkImage(user!.avatarUrl!)
: null,
child: user?.avatarUrl == null
? const Icon(
Icons.person,
size: 48,
color: AppColors.textHint,
)
: null,
),
), ),
const SizedBox(height: 20), const SizedBox(height: 18),
Text( _InfoPanel(
name, children: [
style: const TextStyle( _InfoRow(
fontSize: 24, icon: Icons.badge_rounded,
fontWeight: FontWeight.w700, label: '姓名',
color: AppColors.textPrimary, value: name,
), colors: const [Color(0xFF7DD3FC), Color(0xFF2563EB)],
),
const _SoftDivider(),
_InfoRow(
icon: Icons.phone_iphone_rounded,
label: '手机号',
value: phone.isNotEmpty ? phone : '未绑定手机',
colors: const [Color(0xFFA78BFA), Color(0xFF7C3AED)],
),
const _SoftDivider(),
_InfoRow(
icon: Icons.folder_shared_rounded,
label: '健康档案',
value: '查看和维护基础健康资料',
colors: const [Color(0xFF2DD4BF), Color(0xFF0F766E)],
onTap: () => pushRoute(ref, 'healthArchive'),
),
],
), ),
const SizedBox(height: 6), const SizedBox(height: 18),
Container( _InfoPanel(
padding: const EdgeInsets.symmetric( children: [
horizontal: 14, _InfoRow(
vertical: 6, icon: Icons.verified_user_rounded,
), label: '隐私保护',
decoration: BoxDecoration( value: '健康数据仅用于个人健康管理',
gradient: AppColors.surfaceGradient, colors: const [Color(0xFFFBBF24), Color(0xFFEA580C)],
borderRadius: BorderRadius.circular(20), ),
border: Border.all(color: AppColors.borderLight), ],
boxShadow: AppColors.cardShadowLight,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.phone_android,
size: 16,
color: AppColors.textSecondary,
),
const SizedBox(width: 6),
Text(
phone.isNotEmpty ? phone : '未绑定手机',
style: const TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
),
),
],
),
), ),
const SizedBox(height: 28),
const SizedBox(height: 32), OutlinedButton.icon(
onPressed: () => _logout(context, ref),
// 基础信息卡片 icon: const Icon(Icons.logout_rounded, size: 19),
Container( label: const Text('退出登录'),
width: double.infinity, style: OutlinedButton.styleFrom(
padding: const EdgeInsets.all(20), foregroundColor: AppColors.error,
decoration: BoxDecoration( side: BorderSide(
gradient: AppColors.surfaceGradient, color: AppColors.error.withValues(alpha: 0.34),
borderRadius: BorderRadius.circular(AppTheme.rLg), ),
border: Border.all(color: AppColors.borderLight), minimumSize: const Size.fromHeight(52),
boxShadow: AppColors.cardShadowLight, textStyle: const TextStyle(
), fontSize: 16,
child: Column( fontWeight: FontWeight.w800,
children: [ ),
_infoRow(Icons.person_outline, '姓名', name), shape: RoundedRectangleBorder(
const Divider(height: 24, color: AppColors.borderLight), borderRadius: BorderRadius.circular(18),
_infoRow(
Icons.phone_android,
'手机号',
phone.isNotEmpty ? phone : '未绑定',
),
const Divider(height: 24, color: AppColors.borderLight),
_infoRow(
Icons.health_and_safety,
'健康档案',
'查看详情',
onTap: () => pushRoute(ref, 'healthArchive'),
),
],
),
),
const SizedBox(height: 40),
// 退出登录
SizedBox(
width: 200,
child: OutlinedButton.icon(
onPressed: () => _logout(context, ref),
icon: const Icon(Icons.logout, size: 18),
label: const Text('退出登录', style: TextStyle(fontSize: 16)),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.error,
side: const BorderSide(color: AppColors.error),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
), ),
), ),
), ),
const SizedBox(height: 40),
], ],
), ),
), ),
@@ -145,61 +103,6 @@ class ProfilePage extends ConsumerWidget {
); );
} }
Widget _infoRow(
IconData icon,
String label,
String value, {
VoidCallback? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: BorderRadius.circular(12),
boxShadow: AppColors.cardShadowLight,
),
child: Icon(icon, size: 22, color: Colors.white),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 13,
color: AppColors.textHint,
),
),
const SizedBox(height: 3),
Text(
value,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
),
],
),
),
if (onTap != null)
const Icon(
Icons.chevron_right,
size: 20,
color: AppColors.textHint,
),
],
),
);
}
Future<void> _logout(BuildContext context, WidgetRef ref) async { Future<void> _logout(BuildContext context, WidgetRef ref) async {
final ok = await showDialog<bool>( final ok = await showDialog<bool>(
context: context, context: context,
@@ -227,3 +130,200 @@ class ProfilePage extends ConsumerWidget {
} }
} }
} }
class _ProfileHero extends StatelessWidget {
final String name;
final String phone;
final String? avatarUrl;
const _ProfileHero({
required this.name,
required this.phone,
required this.avatarUrl,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFEEF2FF), Color(0xFFF7ECFF), Color(0xFFEFFBF9)],
),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.white, width: 1.5),
boxShadow: [
BoxShadow(
color: const Color(0xFF6D5DF6).withValues(alpha: 0.10),
blurRadius: 24,
offset: const Offset(0, 14),
),
],
),
child: Row(
children: [
Container(
width: 76,
height: 76,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF38BDF8), Color(0xFFC084FC)],
),
borderRadius: BorderRadius.circular(28),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: avatarUrl != null
? Image.network(avatarUrl!, fit: BoxFit.cover)
: const ColoredBox(
color: Colors.white,
child: Icon(
Icons.person_rounded,
color: Color(0xFF7C3AED),
size: 42,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w900,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 7),
Text(
phone.isNotEmpty ? phone : '未绑定手机',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.textSecondary,
),
),
],
),
),
],
),
);
}
}
class _InfoPanel extends StatelessWidget {
final List<Widget> children;
const _InfoPanel({required this.children});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.88),
borderRadius: BorderRadius.circular(26),
border: Border.all(color: Colors.white, width: 1.4),
boxShadow: AppColors.cardShadowLight,
),
child: Column(children: children),
);
}
}
class _InfoRow extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final List<Color> colors;
final VoidCallback? onTap;
const _InfoRow({
required this.icon,
required this.label,
required this.value,
required this.colors,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(18),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 12),
child: Row(
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: colors,
),
borderRadius: BorderRadius.circular(16),
),
child: Icon(icon, color: Colors.white, size: 22),
),
const SizedBox(width: 13),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: AppColors.textHint,
),
),
const SizedBox(height: 4),
Text(
value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
],
),
),
if (onTap != null)
const Icon(
Icons.arrow_forward_ios_rounded,
size: 16,
color: AppColors.textHint,
),
],
),
),
);
}
}
class _SoftDivider extends StatelessWidget {
const _SoftDivider();
@override
Widget build(BuildContext context) {
return const Divider(height: 1, color: Color(0xFFF1F3F8));
}
}

View File

@@ -1,198 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/app_colors.dart';
import '../../core/app_theme.dart';
import '../../widgets/service_package_card.dart';
class ServicePackageDetailPage extends ConsumerWidget {
final String packageId;
const ServicePackageDetailPage({super.key, required this.packageId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final package = servicePackages.where((p) => p.id == packageId).firstOrNull;
if (package == null) {
return GradientScaffold(
appBar: AppBar(title: const Text('服务包详情')),
body: const Center(child: Text('未找到该服务包')),
);
}
return GradientScaffold(
appBar: AppBar(
title: Text(package.title, style: const TextStyle(fontSize: 19)),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头部卡片
Container(
width: double.infinity,
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
package.headerColor,
package.headerColor.withAlpha(180),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: AppColors.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white.withAlpha(40),
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'VIP 产品权益',
style: TextStyle(
fontSize: 15,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 12),
Text(
package.title,
style: const TextStyle(
fontSize: 25,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
const SizedBox(height: 8),
Text(
package.subtitle,
style: TextStyle(
fontSize: 17,
color: Colors.white.withAlpha(200),
),
),
],
),
),
// 服务图标
Padding(
padding: const EdgeInsets.all(20),
child: Wrap(
spacing: 16,
runSpacing: 16,
children: package.services
.map(
(s) => SizedBox(
width: (MediaQuery.of(context).size.width - 72) / 4,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(14),
),
child: Icon(
s.icon,
size: 25,
color: AppTheme.primary,
),
),
const SizedBox(height: 6),
Text(
s.label,
style: const TextStyle(
fontSize: 15,
color: AppTheme.textSub,
),
textAlign: TextAlign.center,
),
],
),
),
)
.toList(),
),
),
// 适用人群
_Section(
title: '适用人群',
child: Text(
package.targetAudience,
style: const TextStyle(
fontSize: 17,
color: AppTheme.textSub,
height: 1.6,
),
),
),
// 详细说明
...package.detailSections.map(
(s) => _Section(
title: s.title,
child: Text(
s.content,
style: const TextStyle(
fontSize: 17,
color: AppTheme.textSub,
height: 1.6,
),
),
),
),
const SizedBox(height: 40),
],
),
),
);
}
}
class _Section extends StatelessWidget {
final String title;
final Widget child;
const _Section({required this.title, required this.child});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.border),
boxShadow: AppColors.cardShadowLight,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 19,
fontWeight: FontWeight.w600,
color: AppTheme.text,
),
),
const SizedBox(height: 10),
child,
],
),
);
}
}

View File

@@ -5,60 +5,364 @@ import '../../core/app_colors.dart';
import '../../core/app_theme.dart'; import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart'; import '../../providers/auth_provider.dart';
import '../../widgets/app_menu_item.dart'; import '../../providers/data_providers.dart' show apiClientProvider;
class SettingsPage extends ConsumerWidget { class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { @override
Widget build(BuildContext context, WidgetRef ref) {
return GradientScaffold( return GradientScaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: AppColors.cardBackground, backgroundColor: Colors.white.withValues(alpha: 0.86),
leading: IconButton(icon: const Icon(LucideIcons.chevronLeft, color: AppColors.textPrimary), onPressed: () => popRoute(ref)), leading: IconButton(
title: const Text('设置', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), icon: const Icon(
LucideIcons.chevronLeft,
color: AppColors.textPrimary,
),
onPressed: () => popRoute(ref),
),
title: const Text(
'设置',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
centerTitle: true, centerTitle: true,
), ),
body: SafeArea(child: SingleChildScrollView( body: SafeArea(
padding: const EdgeInsets.only(bottom: 30), child: SingleChildScrollView(
child: Column(children: [ padding: const EdgeInsets.fromLTRB(20, 18, 20, 34),
const SizedBox(height: AppTheme.sMd), child: Column(
AppMenuItem(icon: LucideIcons.bell, title: '消息通知', onTap: () => pushRoute(ref, 'notificationPrefs')), crossAxisAlignment: CrossAxisAlignment.stretch,
AppMenuItem(icon: LucideIcons.type, title: '字体大小', subtitle: '老年版', onTap: () {}), children: [
AppMenuItem(icon: LucideIcons.sprayCan, title: '清除缓存', onTap: () {}), const _SettingsHeader(),
AppMenuItem(icon: LucideIcons.info, title: '关于健康管家', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})), const SizedBox(height: 18),
AppMenuItem(icon: LucideIcons.shield, title: '隐私协议', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'privacy'})), _SettingsGroup(
const SizedBox(height: 30), title: '健康与设备',
GestureDetector( children: [
onTap: () => _logout(context, ref), _SettingsTile(
child: Container( icon: LucideIcons.bluetooth,
margin: const EdgeInsets.symmetric(horizontal: AppTheme.rXl), title: '蓝牙设备',
height: 50, colors: const [Color(0xFF38BDF8), Color(0xFF2563EB)],
alignment: Alignment.center, onTap: () => pushRoute(ref, 'devices'),
decoration: BoxDecoration( ),
border: Border.all(color: AppColors.error.withAlpha(80)), _SettingsTile(
borderRadius: BorderRadius.circular(AppTheme.rPill), icon: LucideIcons.bell,
title: '消息通知',
colors: const [Color(0xFFA78BFA), Color(0xFF7C3AED)],
onTap: () => pushRoute(ref, 'notificationPrefs'),
),
],
), ),
child: const Text('退出登录', style: TextStyle(fontSize: 19, color: AppColors.error, fontWeight: FontWeight.w500)), const SizedBox(height: 16),
), _SettingsGroup(
title: '使用偏好',
children: [
_SettingsTile(
icon: LucideIcons.type,
title: '字体大小',
subtitle: '老年友好',
colors: const [Color(0xFFFBBF24), Color(0xFFEA580C)],
onTap: () {},
),
_SettingsTile(
icon: LucideIcons.sprayCan,
title: '清除缓存',
colors: const [Color(0xFF2DD4BF), Color(0xFF0F766E)],
onTap: () {},
),
],
),
const SizedBox(height: 16),
_SettingsGroup(
title: '关于',
children: [
_SettingsTile(
icon: LucideIcons.info,
title: '关于健康管家',
colors: const [Color(0xFF60A5FA), Color(0xFF0891B2)],
onTap: () =>
pushRoute(ref, 'staticText', params: {'type': 'about'}),
),
_SettingsTile(
icon: LucideIcons.shield,
title: '隐私协议',
colors: const [Color(0xFFF472B6), Color(0xFFDB2777)],
onTap: () => pushRoute(
ref,
'staticText',
params: {'type': 'privacy'},
),
),
],
),
const SizedBox(height: 14),
OutlinedButton.icon(
onPressed: () => _deleteAccount(context, ref),
icon: const Icon(LucideIcons.trash, size: 19),
label: const Text('删除账号'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.error,
side: BorderSide(color: AppColors.error.withValues(alpha: 0.34)),
minimumSize: const Size.fromHeight(52),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
),
),
const SizedBox(height: 10),
ElevatedButton.icon(
onPressed: () => _logout(context, ref),
icon: const Icon(LucideIcons.logOut, size: 19),
label: const Text('退出登录'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.error,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(52),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
),
],
), ),
]), ),
)), ),
); );
} }
Future<void> _deleteAccount(BuildContext context, WidgetRef ref) async {
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)),
title: const Text('删除账号', style: TextStyle(color: AppColors.error)),
content: const Text('此操作不可恢复,将删除所有健康数据、用药记录、饮食记录、报告等,确定继续?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确认删除', style: TextStyle(color: AppColors.error))),
],
),
);
if (ok == true) {
await ref.read(apiClientProvider).delete('/api/user/account');
await ref.read(authProvider.notifier).logout();
goRoute(ref, 'login');
}
}
Future<void> _logout(BuildContext context, WidgetRef ref) async { Future<void> _logout(BuildContext context, WidgetRef ref) async {
final ok = await showDialog<bool>( final ok = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.rXl),
),
title: const Text('退出登录'), title: const Text('退出登录'),
content: const Text('确定退出?'), content: const Text('确定退出当前账号'),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), TextButton(
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定', style: TextStyle(color: AppColors.error))), onPressed: () => Navigator.pop(ctx, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('确定', style: TextStyle(color: AppColors.error)),
),
], ],
), ),
); );
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); } if (ok == true) {
await ref.read(authProvider.notifier).logout();
goRoute(ref, 'login');
}
}
}
class _SettingsHeader extends StatelessWidget {
const _SettingsHeader();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFEEF2FF), Color(0xFFF7ECFF), Color(0xFFEFFBF9)],
),
borderRadius: BorderRadius.circular(28),
border: Border.all(color: Colors.white, width: 1.5),
),
child: Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF38BDF8), Color(0xFFC084FC)],
),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
LucideIcons.settings,
color: Colors.white,
size: 25,
),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'偏好设置',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: AppColors.textPrimary,
),
),
SizedBox(height: 5),
Text(
'管理设备、通知和账号安全',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
],
),
),
],
),
);
}
}
class _SettingsGroup extends StatelessWidget {
final String title;
final List<Widget> children;
const _SettingsGroup({required this.title, required this.children});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w900,
color: AppColors.textSecondary,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.88),
borderRadius: BorderRadius.circular(26),
border: Border.all(color: Colors.white, width: 1.4),
boxShadow: AppColors.cardShadowLight,
),
child: Column(
children: [
for (var i = 0; i < children.length; i++) ...[
children[i],
if (i != children.length - 1)
const Divider(height: 1, color: Color(0xFFF1F3F8)),
],
],
),
),
],
);
}
}
class _SettingsTile extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final List<Color> colors;
final VoidCallback onTap;
const _SettingsTile({
required this.icon,
required this.title,
required this.colors,
required this.onTap,
this.subtitle,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(18),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 12),
child: Row(
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: colors,
),
borderRadius: BorderRadius.circular(16),
),
child: Icon(icon, color: Colors.white, size: 21),
),
const SizedBox(width: 13),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 3),
Text(
subtitle!,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.textHint,
),
),
],
],
),
),
const Icon(
Icons.arrow_forward_ios_rounded,
size: 16,
color: AppColors.textHint,
),
],
),
),
);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,249 +0,0 @@
import 'package:flutter/material.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// 服务包数据模型
class ServicePackage {
final String id;
final String title;
final String subtitle;
final Color headerColor;
final List<ServiceItem> services;
final String targetAudience;
final List<DetailSection> detailSections;
const ServicePackage({
required this.id,
required this.title,
required this.subtitle,
required this.headerColor,
required this.services,
required this.targetAudience,
required this.detailSections,
});
}
class ServiceItem {
final IconData icon;
final String label;
const ServiceItem({required this.icon, required this.label});
}
class DetailSection {
final String title;
final String content;
const DetailSection({required this.title, required this.content});
}
/// 预定义服务包数据 —— 基于项目实际功能
final List<ServicePackage> servicePackages = [
ServicePackage(
id: 'vip_comprehensive',
title: '心血管健康管理服务包',
subtitle: 'VIP 产品权益',
headerColor: const Color(0xFF4A90D9),
services: [
ServiceItem(icon: Icons.phone_in_talk_outlined, label: '电话咨询'),
ServiceItem(icon: Icons.chat_bubble_outline, label: '在线咨询'),
ServiceItem(icon: Icons.calendar_month_outlined, label: '个性化随访'),
ServiceItem(icon: Icons.medication_outlined, label: '调药管理'),
ServiceItem(icon: Icons.monitor_heart_outlined, label: '风险监测'),
ServiceItem(icon: Icons.devices_outlined, label: '智能硬件'),
ServiceItem(icon: Icons.family_restroom_outlined, label: '亲情账号'),
ServiceItem(icon: Icons.verified_user_outlined, label: '健康保障'),
],
targetAudience:
'心血管疾病患者(如高血压、冠心病、心律失常等)\n'
'高危人群(如高血脂、糖尿病、肥胖、长期吸烟饮酒者)\n'
'术后康复人群(如心脏支架/搭桥术后、PCI术后患者',
detailSections: [
DetailSection(
title: '1、个性化康复管理',
content:
'设定动态化的危险因素管理目标,根据患者的住院数据(如基础疾病、高危因素、合并症、当前用药等),确定高危因素(如高脂血症、高血糖等),设定高危因子(如低密度脂蛋白等)的管控目标。基于药物不良反应,制定个性化管理及复查方案。',
),
DetailSection(
title: '2、家庭医生专业团队',
content:
'由心内科主诊医生团队联合康复管理团队共同参与患者管理,将疾病康复从院内延伸至院外。团队由专科医生、康复治疗师、健康管理师、营养师等多学科成员组成。',
),
DetailSection(
title: '3、全年不限次数在线咨询',
content:
'为患者提供全年不限次数的咨询服务,支持微信(如文字、图文、语音等)、电话咨询等方式。如检查报告解读、用药咨询、病情咨询、饮食咨询、心理咨询等。',
),
DetailSection(
title: '4、主动跟踪随访管理',
content:
'动态临床评估:动态评估疾病复发、出血、致死致残等风险,实时优化管理方案。\n'
'主动症状管理:全年主动跟踪患者临床症状,实现疾病恶化早发现、早处理。\n'
'精准药物管理:全程用药指导,严密监控药物副作用,及时处理药物不良反应,确保药物治疗效果。\n'
'生活方式干预:个性化指导患者合理膳食、运动康复、心理调节、戒烟限酒等。',
),
DetailSection(
title: '5、可穿戴智能设备',
content:
'配备可穿戴智能监测设备,患者居家测量后实时上传管理中心,医生和家属均可远程实时监测数据(血压和心率),记录数据变化趋势,智能预警,及时干预异常指标,降低不良事件发生率。',
),
DetailSection(
title: '6、亲情账号联动管理',
content:
'支持5名家属参与管理实时多端同步患者病情医生、患者、家属三方共享患者安心家属放心。',
),
DetailSection(
title: '7、健康档案与报告分析',
content:
'自动整合血压、心率、血糖、血氧、体重等健康数据,生成可视化健康趋势报告。\n'
'AI 智能分析健康数据变化,提前预警潜在风险,提供个性化健康建议。',
),
],
),
ServicePackage(
id: 'vip_premium',
title: '心力衰竭专项管理服务包',
subtitle: 'VIP 产品权益',
headerColor: const Color(0xFF2BA87E),
services: [
ServiceItem(icon: Icons.phone_in_talk_outlined, label: '电话咨询'),
ServiceItem(icon: Icons.chat_bubble_outline, label: '在线咨询'),
ServiceItem(icon: Icons.calendar_month_outlined, label: '个性化随访'),
ServiceItem(icon: Icons.medication_outlined, label: '调药管理'),
ServiceItem(icon: Icons.monitor_heart_outlined, label: '风险监测'),
ServiceItem(icon: Icons.devices_outlined, label: '智能硬件'),
ServiceItem(icon: Icons.family_restroom_outlined, label: '亲情账号'),
],
targetAudience:
'心力衰竭患者\n'
'心力衰竭高危人群(心肌梗死、瓣膜病、心肌病、高血压、代谢综合征等)',
detailSections: [
DetailSection(
title: '1、个性化康复管理',
content:
'设定动态化的危险因素管理目标,根据患者的住院数据(如基础疾病、高危因素、合并症、当前用药等),确定高危因素管控目标。基于药物不良反应,制定个性化管理及复查方案。',
),
DetailSection(
title: '2、家庭医生专业团队',
content:
'由心内科主诊医生团队联合哈瑞特医疗院外康复管理团队共同参与患者管理,将疾病康复从院内延伸至院外,哈瑞特医疗康复管理团队由专科医生、康复治疗师、健康管理师、营养师等多学科成员组成。',
),
DetailSection(
title: '3、全年不限次数在线咨询',
content:
'为患者提供全年不限次数的咨询服务,支持微信(如文字、图文、语音等)、电话咨询(400-1666199),如检查报告解读、用药咨询、病情咨询、饮食咨询、心理咨询等。',
),
DetailSection(
title: '4、主动跟踪随访管理',
content:
'动态临床评估:动态评估疾病复发、出血、致死致残等风险,实时优化管理方案。\n'
'主动症状管理:全年主动跟踪患者临床症状,实现疾病恶化早发现、早处理。\n'
'精准药物管理:全程用药指导,严密监控药物副作用,及时处理药物不良反应,确保药物治疗效果。\n'
'生活方式干预:个性化指导患者合理膳食、运动康复、心理调节、戒烟限酒等。',
),
DetailSection(
title: '5、可穿戴智能设备',
content:
'配备可穿戴智能监测设备,患者居家测量后实时上传管理中心,医生和家属均可远程实时监测数据(血压和心率),记录数据变化趋势,智能预警,及时干预异常指标,降低不良事件发生率。',
),
DetailSection(
title: '6、亲情账号联动管理',
content:
'支持5名家属参与管理实时多端同步患者病情医生、患者、家属三方共享患者安心家属放心。',
),
],
),
];
/// 服务包卡片
class ServicePackageCard extends ConsumerWidget {
final bool compact;
const ServicePackageCard({super.key, this.compact = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final package = servicePackages.first;
// 构建服务图标网格
final services = package.services.take(8).toList();
final serviceRows = <Widget>[];
for (var i = 0; i < services.length; i += 4) {
final rowItems = services.skip(i).take(4).toList();
serviceRows.add(Padding(
padding: EdgeInsets.only(top: i > 0 ? 12 : 0),
child: Row(
children: rowItems.map((item) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFFFFF8EE),
shape: BoxShape.circle,
),
child: Icon(item.icon, size: 23, color: const Color(0xFFF5A623)),
),
const SizedBox(height: 4),
Text(item.label,
style: const TextStyle(fontSize: 14, color: AppTheme.textSub),
textAlign: TextAlign.center,
),
],
),
);
}).toList(),
),
));
}
Widget card = GestureDetector(
onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}),
child: Padding(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行 + 详情入口
Row(children: [
Text(package.title, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: AppTheme.text)),
const Spacer(),
GestureDetector(
onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text('详情', style: TextStyle(fontSize: 16, color: AppTheme.textSub)),
const SizedBox(width: 2),
Icon(Icons.chevron_right, size: 21, color: AppTheme.textHint),
]),
),
]),
const SizedBox(height: 12),
// 服务图标网格
...serviceRows,
],
),
),
);
if (compact) return card;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(8),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: card,
);
}
}