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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../core/app_colors.dart';
import '../../../core/app_theme.dart';
import '../../../core/navigation_provider.dart';
@@ -294,12 +295,27 @@ class ChatMessagesView extends ConsumerWidget {
fit: BoxFit.cover,
),
// ── 医生选择区(问诊专用)──
// ── AI问诊引导 ──
if (agent == ActiveAgent.consultation) ...[
const SizedBox(height: 14),
Padding(
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,
children: [
ShaderMask(
shaderCallback: (bounds) => AppColors.actionOutlineGradient.createShader(bounds),
shaderCallback: (bounds) =>
AppColors.actionOutlineGradient.createShader(bounds),
child: Icon(a.icon, size: 24, color: Colors.white),
),
const SizedBox(width: 10),
Text(
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 — 统一确认卡片(紫白蓝风格)
// ═══════════════════════════════════════════════════════════
@@ -1556,46 +1470,52 @@ class ChatMessagesView extends ConsumerWidget {
static _AgentColors _agentColors(ActiveAgent agent) {
return switch (agent) {
ActiveAgent.consultation => _AgentColors(
gradient: [AppColors.sageAccent, AppColors.primary],
bg: const Color(0xFFF2FBF9),
border: const Color(0xFFD9F0EA),
iconBg: const Color(0xFFE8F8F4),
accent: const Color(0xFF4F9E90),
gradient: [Color(0xFFFFCAD4), Color(0xFFFF9AAE)],
bg: const Color(0xFFFFF4F6),
border: const Color(0xFFFFDFE7),
iconBg: const Color(0xFFFFEEF2),
accent: const Color(0xFFFF7F98),
verticalGradient: true,
),
ActiveAgent.health => _AgentColors(
gradient: [AppColors.meadowAccent, AppColors.auraDeepIndigo],
bg: const Color(0xFFF3FBF4),
border: const Color(0xFFDFF2E4),
iconBg: const Color(0xFFEFF9F0),
accent: const Color(0xFF4EA65B),
gradient: [Color(0xFFFFAFBD), Color(0xFFC9FFBF)],
bg: const Color(0xFFF7FDEB),
border: const Color(0xFFEAF7BF),
iconBg: const Color(0xFFF9FCEB),
accent: const Color(0xFF8BD982),
verticalGradient: true,
),
ActiveAgent.diet => _AgentColors(
gradient: [AppColors.peachAccent, AppColors.auraOrchid],
gradient: [Color(0xFFF6D365), Color(0xFFFDA085)],
bg: const Color(0xFFFFF7F0),
border: const Color(0xFFFFE8D4),
iconBg: const Color(0xFFFFF0E4),
accent: const Color(0xFFE26A64),
accent: const Color(0xFFF89C5B),
verticalGradient: true,
),
ActiveAgent.medication => _AgentColors(
gradient: [AppColors.auraLavender, AppColors.blueMeasure],
gradient: [Color(0xFF89F7FE), Color(0xFF66A6FF)],
bg: const Color(0xFFF4F6FF),
border: const Color(0xFFDDE6FF),
iconBg: const Color(0xFFEFF3FF),
accent: AppColors.primary,
accent: const Color(0xFF4F8FF7),
verticalGradient: true,
),
ActiveAgent.report => _AgentColors(
gradient: [AppColors.primary, AppColors.roseAccent],
gradient: [Color(0xFFE0C3FC), Color(0xFF8EC5FC)],
bg: const Color(0xFFF8F5FF),
border: const Color(0xFFE9E0FF),
iconBg: const Color(0xFFF2EDFF),
accent: AppColors.primary,
accent: const Color(0xFF9D8AF8),
verticalGradient: true,
),
ActiveAgent.exercise => _AgentColors(
gradient: [AppColors.sageAccent, AppColors.auraDeepIndigo],
bg: const Color(0xFFF0FBFD),
border: const Color(0xFFD6EEF2),
iconBg: const Color(0xFFE8F7FA),
accent: const Color(0xFF2B8EA0),
gradient: [Color(0xFFB6F2FF), Color(0xFF7DD3FC)],
bg: const Color(0xFFF0FCFF),
border: const Color(0xFFD8F5FC),
iconBg: const Color(0xFFEAFBFF),
accent: const Color(0xFF38BDE8),
verticalGradient: true,
),
_ => _AgentColors(
gradient: [AppColors.primary, AppColors.blueMeasure],
@@ -1609,16 +1529,16 @@ class ChatMessagesView extends ConsumerWidget {
static (_AgentIcon, String, String) _agentInfo(ActiveAgent agent) {
return switch (agent) {
ActiveAgent.health => (Icons.favorite_border, '记数据', '录入血压、血糖、心率等日常指标'),
ActiveAgent.diet => (Icons.restaurant, '拍饮食', '拍照识别食物热量和营养成分'),
ActiveAgent.medication => (Icons.medication, '药管家', '管理药品、提醒服药、追踪用量'),
ActiveAgent.health => (LucideIcons.heartPulse, '记数据', '录入血压、血糖、心率等日常指标'),
ActiveAgent.diet => (LucideIcons.utensils, '拍饮食', '拍照识别食物热量和营养成分'),
ActiveAgent.medication => (LucideIcons.pill, '药管家', '管理药品、提醒服药、追踪用量'),
ActiveAgent.consultation => (
Icons.local_hospital,
'问诊',
'在线咨询医生,描述症状获取建议',
LucideIcons.messageCircle,
'AI问诊',
'AI智能问诊,描述症状获取建议',
),
ActiveAgent.report => (Icons.assignment, '报告分析', '上传体检报告AI 辅助解读'),
ActiveAgent.exercise => (Icons.directions_run, '运动', '制定运动计划,打卡记录进度'),
ActiveAgent.report => (LucideIcons.fileText, '报告分析', '上传体检报告AI 辅助解读'),
ActiveAgent.exercise => (LucideIcons.activity, '运动', '制定运动计划,打卡记录进度'),
_ => (Icons.forum_outlined, 'AI 助手', '您的智能健康管家'),
};
}
@@ -2031,13 +1951,17 @@ class _AgentMark extends StatelessWidget {
decoration: BoxDecoration(
gradient: LinearGradient(
colors: colors.gradient,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
begin: colors.verticalGradient
? Alignment.topCenter
: Alignment.topLeft,
end: colors.verticalGradient
? Alignment.bottomCenter
: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(colors.verticalGradient ? 18 : 20),
boxShadow: [
BoxShadow(
color: colors.accent.withValues(alpha: 0.24),
color: colors.accent.withValues(alpha: 0.18),
blurRadius: 18,
offset: const Offset(0, 8),
),
@@ -2103,12 +2027,14 @@ class _AgentColors {
final Color border;
final Color iconBg;
final Color accent;
final bool verticalGradient;
const _AgentColors({
required this.gradient,
required this.bg,
required this.border,
required this.iconBg,
required this.accent,
this.verticalGradient = false,
});
}

View File

@@ -17,127 +17,85 @@ class ProfilePage extends ConsumerWidget {
return GradientScaffold(
appBar: AppBar(
backgroundColor: Colors.white.withValues(alpha: 0.9),
title: const Text('个人信息'),
backgroundColor: Colors.white.withValues(alpha: 0.86),
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(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 34),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 20),
// 头像区
Center(
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,
),
_ProfileHero(
name: name,
phone: phone,
avatarUrl: user?.avatarUrl,
),
const SizedBox(height: 20),
Text(
name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
const SizedBox(height: 18),
_InfoPanel(
children: [
_InfoRow(
icon: Icons.badge_rounded,
label: '姓名',
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),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 6,
),
decoration: BoxDecoration(
gradient: AppColors.surfaceGradient,
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: 18),
_InfoPanel(
children: [
_InfoRow(
icon: Icons.verified_user_rounded,
label: '隐私保护',
value: '健康数据仅用于个人健康管理',
colors: const [Color(0xFFFBBF24), Color(0xFFEA580C)],
),
],
),
const SizedBox(height: 32),
// 基础信息卡片
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppColors.surfaceGradient,
borderRadius: BorderRadius.circular(AppTheme.rLg),
border: Border.all(color: AppColors.borderLight),
boxShadow: AppColors.cardShadowLight,
),
child: Column(
children: [
_infoRow(Icons.person_outline, '姓名', name),
const Divider(height: 24, color: AppColors.borderLight),
_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: 28),
OutlinedButton.icon(
onPressed: () => _logout(context, ref),
icon: const Icon(Icons.logout_rounded, 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: 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 {
final ok = await showDialog<bool>(
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/navigation_provider.dart';
import '../../providers/auth_provider.dart';
import '../../widgets/app_menu_item.dart';
import '../../providers/data_providers.dart' show apiClientProvider;
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key});
@override Widget build(BuildContext context, WidgetRef ref) {
@override
Widget build(BuildContext context, WidgetRef ref) {
return GradientScaffold(
appBar: AppBar(
backgroundColor: AppColors.cardBackground,
leading: IconButton(icon: const Icon(LucideIcons.chevronLeft, color: AppColors.textPrimary), onPressed: () => popRoute(ref)),
title: const Text('设置', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
backgroundColor: Colors.white.withValues(alpha: 0.86),
leading: IconButton(
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,
),
body: SafeArea(child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 30),
child: Column(children: [
const SizedBox(height: AppTheme.sMd),
AppMenuItem(icon: LucideIcons.bell, title: '消息通知', onTap: () => pushRoute(ref, 'notificationPrefs')),
AppMenuItem(icon: LucideIcons.type, title: '字体大小', subtitle: '老年版', onTap: () {}),
AppMenuItem(icon: LucideIcons.sprayCan, title: '清除缓存', onTap: () {}),
AppMenuItem(icon: LucideIcons.info, title: '关于健康管家', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})),
AppMenuItem(icon: LucideIcons.shield, title: '隐私协议', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'privacy'})),
const SizedBox(height: 30),
GestureDetector(
onTap: () => _logout(context, ref),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: AppTheme.rXl),
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(color: AppColors.error.withAlpha(80)),
borderRadius: BorderRadius.circular(AppTheme.rPill),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 34),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _SettingsHeader(),
const SizedBox(height: 18),
_SettingsGroup(
title: '健康与设备',
children: [
_SettingsTile(
icon: LucideIcons.bluetooth,
title: '蓝牙设备',
colors: const [Color(0xFF38BDF8), Color(0xFF2563EB)],
onTap: () => pushRoute(ref, 'devices'),
),
_SettingsTile(
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 {
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.rXl),
),
title: const Text('退出登录'),
content: const Text('确定退出?'),
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))),
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(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,
),
],
),
),
);
}
}