feat: 蓝牙血压计基础架构 + UI配色体系升级

- 新增 AppColors 配色体系(紫蓝渐变 + 超大圆角 + 漂浮卡片)
- 新增 OmronBleService(BLE 扫描/连接/SFLOAT 协议解析)
- 新增 BpReading 数据模型 + 血压数据自动同步后端
- 新增 DeviceScanPage(血压计扫描绑定页)+ DeviceBindPage(已绑定管理页)
- 侧边栏新增"蓝牙设备"入口,ProfilePage 移除设备管理
- Android/iOS BLE 权限配置(BLUETOOTH_SCAN/CONNECT)
- AppTheme 升级:新阴影系统、新语义色、圆角放大
This commit is contained in:
MingNian
2026-06-09 18:48:58 +08:00
parent f01fc9268d
commit 3964cf2bcb
14 changed files with 1068 additions and 157 deletions

View File

@@ -1,161 +1,197 @@
import 'package:flutter/material.dart';
/// 统一配色方案 - 清爽紫蓝风格(参考蚂蚁阿福)
/// 统一配色方案 - 蚂蚁阿福风格:紫蓝渐变 + 淡紫背景 + 圆角卡片
class AppColors {
AppColors._();
// ═══════════════════════════════════════════════════════════
// 主色调
// 主色调 - 紫蓝渐变系
// ═══════════════════════════════════════════════════════════
/// 纯白背景
static const Color background = Color(0xFFFFFFFF);
/// 主紫色(鲜艳紫蓝,阿福风主按钮色)
static const Color primary = Color(0xFF6366F1);
/// 次级背景色(非常淡的蓝紫色)
static const Color backgroundSecondary = Color(0xFFF8FAFF);
/// 主紫色深色
static const Color primaryDark = Color(0xFF4F46E5);
/// 卡片背景
/// 浅紫(用于背景、图标底)
static const Color primaryLight = Color(0xFFEEF2FF);
/// 中紫(渐变过渡)
static const Color primaryMid = Color(0xFFA78BFA);
/// 青色点缀(渐变右侧,标签色)
static const Color accentCyan = Color(0xFF22D3EE);
/// 青色深色
static const Color accentCyanDark = Color(0xFF06B6D4);
// ═══════════════════════════════════════════════════════════
// 背景色
// ═══════════════════════════════════════════════════════════
/// 页面背景(极淡紫蓝,营造漂浮感)
static const Color background = Color(0xFFF8FAFF);
/// 更淡的紫背景(最底层,带渐变感)
static const Color backgroundSoft = Color(0xFFF5F3FF);
/// 卡片背景 - 纯白
static const Color cardBackground = Color(0xFFFFFFFF);
// ═══════════════════════════════════════════════════════════
// 紫色系(清爽浅紫)
// ═══════════════════════════════════════════════════════════
/// 主紫色(浅紫,蚂蚁阿福风格)
static const Color primaryPurple = Color(0xFFB8C4FF);
/// 深一点的紫色(用于强调)
static const Color primaryPurpleDark = Color(0xFF8BA4E8);
/// 紫色渐变起点
static const Color purpleGradientStart = Color(0xFFD4DFFF);
/// 紫色渐变终点
static const Color purpleGradientEnd = Color(0xFFB8C4FF);
/// 卡片内小卡片背景(极淡紫灰)
static const Color cardInner = Color(0xFFF8FAFF);
// ═══════════════════════════════════════════════════════════
// 蓝色系
// 图标渐变配色
// ═══════════════════════════════════════════════════════════
/// 主蓝色(清爽蓝)
static const Color primaryBlue = Color(0xFF5B9FFF);
/// 报告/数据图标:蓝紫渐变
static const Color iconBlueStart = Color(0xFF818CF8);
static const Color iconBlueEnd = Color(0xFF6366F1);
/// 深一点的蓝色
static const Color primaryBlueDark = Color(0xFF3A7FE8);
/// 药管家图标:青蓝渐变
static const Color iconCyanStart = Color(0xFF67E8F9);
static const Color iconCyanEnd = Color(0xFF22D3EE);
/// 蓝色渐变起点
static const Color blueGradientStart = Color(0xFFB8D4FF);
/// 拍皮肤图标:紫蓝渐变
static const Color iconPurpleStart = Color(0xFFA78BFA);
static const Color iconPurpleEnd = Color(0xFF7C3AED);
/// 蓝色渐变终点
static const Color blueGradientEnd = Color(0xFF7BB8FF);
// ═══════════════════════════════════════════════════════════
// 渐变组合
// ═══════════════════════════════════════════════════════════
/// 紫蓝渐变(用于标题背景、图标背景)
static const LinearGradient purpleBlueGradient = LinearGradient(
colors: [purpleGradientStart, blueGradientEnd],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 轻量级渐变(用于卡片内小元素)
static const LinearGradient lightGradient = LinearGradient(
colors: [Color(0xFFEEF2FF), Color(0xFFF0F5FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 通用图标底色
static const Color iconBg = Color(0xFFEEF2FF);
// ═══════════════════════════════════════════════════════════
// 文字颜色
// ═══════════════════════════════════════════════════════════
/// 主要文字(深
static const Color textPrimary = Color(0xFF1A1A2E);
/// 主要文字(深紫黑
static const Color textPrimary = Color(0xFF1E1B4B);
/// 次要文字(中等灰色
static const Color textSecondary = Color(0xFF666666);
/// 次要文字(深灰
static const Color textSecondary = Color(0xFF64748B);
/// 辅助文字(浅灰
static const Color textHint = Color(0xFF999999);
/// 辅助文字(浅灰)
static const Color textHint = Color(0xFF94A3B8);
/// 文字颜色(用于渐变背景上的白色文字
/// 渐变上的白色文字
static const Color textOnGradient = Color(0xFFFFFFFF);
// ═══════════════════════════════════════════════════════════
// 图标和元素颜色
// ═══════════════════════════════════════════════════════════
/// 图标背景色(非常淡的蓝紫色)
static const Color iconBackground = Color(0xFFEEF2FF);
/// 图标主色(清爽蓝紫色)
static const Color iconColor = Color(0xFF5B8DEF);
/// 次要图标色
static const Color iconColorSecondary = Color(0xFF8BA4E8);
// ═══════════════════════════════════════════════════════════
// 边框和分割线
// ═══════════════════════════════════════════════════════════
/// 默认边框色
static const Color border = Color(0xFFE8EBF5);
/// 默认边框色(极淡)
static const Color border = Color(0xFFE2E8F0);
/// 浅边框色
static const Color borderLight = Color(0xFFF0F2F7);
/// 浅边框色
static const Color borderLight = Color(0xFFF1F5F9);
// ═══════════════════════════════════════════════════════════
// 功能色
// ═══════════════════════════════════════════════════════════
/// 成功色(绿
static const Color success = Color(0xFF4CAF50);
/// 成功色(柔和绿)
static const Color success = Color(0xFF22C55E);
/// 成功浅色
static const Color successLight = Color(0xFFDCFCE7);
/// 错误色(柔和红)
static const Color error = Color(0xFFFF6B6B);
static const Color error = Color(0xFFEF4444);
/// 警告色(橙色)
static const Color warning = Color(0xFFFFB74D);
/// 错误浅色
static const Color errorLight = Color(0xFFFEE2E2);
/// 警告色(橙)
static const Color warning = Color(0xFFF59E0B);
/// 警告浅色
static const Color warningLight = Color(0xFFFEF3C7);
// ═══════════════════════════════════════════════════════════
// 按钮渐变(白色底+渐变边框)
// 渐变组
// ═══════════════════════════════════════════════════════════
/// 主按钮渐变边框色
static const LinearGradient buttonGradient = LinearGradient(
colors: [primaryPurple, primaryBlue],
/// 主渐变:紫 → 蓝紫(主按钮填充)
static const LinearGradient primaryGradient = LinearGradient(
colors: [Color(0xFF818CF8), Color(0xFF6366F1)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 边框渐变:蓝紫 → 紫 → 青(按钮/卡片描边)
static const LinearGradient borderGradient = LinearGradient(
colors: [Color(0xFF818CF8), Color(0xFFA78BFA), Color(0xFF22D3EE)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
);
/// 成功按钮渐变
static const LinearGradient successButtonGradient = LinearGradient(
colors: [Color(0xFF81E6A0), success],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
/// 青色渐变(药管家等)
static const LinearGradient cyanGradient = LinearGradient(
colors: [Color(0xFF67E8F9), Color(0xFF22D3EE)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 紫色渐变(拍皮肤等)
static const LinearGradient purpleGradient = LinearGradient(
colors: [Color(0xFFC4B5FD), Color(0xFF8B5CF6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 页面背景渐变(淡紫 → 淡蓝,营造漂浮感)
static const LinearGradient pageBgGradient = LinearGradient(
colors: [Color(0xFFF5F3FF), Color(0xFFEFF6FF)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
);
/// 卡片内轻渐变
static const LinearGradient cardLightGradient = LinearGradient(
colors: [Color(0xFFEEF2FF), Color(0xFFF0F9FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// ═══════════════════════════════════════════════════════════
// 阴影
// ═══════════════════════════════════════════════════════════
/// 卡片阴影
/// 卡片阴影(淡紫色柔和阴影)
static List<BoxShadow> cardShadow = [
BoxShadow(
color: primaryPurple.withAlpha(15),
blurRadius: 20,
offset: const Offset(0, 6),
color: Color(0xFF6366F1).withOpacity(0.08),
blurRadius: 24,
offset: const Offset(0, 8),
),
];
/// 轻卡片阴影
static List<BoxShadow> cardShadowLight = [
BoxShadow(
color: Color(0xFF6366F1).withOpacity(0.05),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
/// 按钮阴影
static List<BoxShadow> buttonShadow = [
BoxShadow(
color: primaryPurple.withAlpha(20),
blurRadius: 12,
offset: const Offset(0, 4),
color: Color(0xFF6366F1).withOpacity(0.25),
blurRadius: 16,
offset: const Offset(0, 6),
),
];
/// 悬浮操作按钮阴影(右下角大号按钮)
static List<BoxShadow> fabShadow = [
BoxShadow(
color: Color(0xFF6366F1).withOpacity(0.35),
blurRadius: 20,
offset: const Offset(0, 8),
),
];
}

View File

@@ -14,6 +14,7 @@ import '../pages/settings/notification_prefs_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/device/device_scan_page.dart';
import '../pages/remaining_pages.dart';
/// 根据路由信息返回对应页面
@@ -54,6 +55,8 @@ Widget buildPage(RouteInfo route) {
return const ProfilePage();
case 'devices':
return const DeviceManagementPage();
case 'deviceScan':
return const DeviceScanPage();
case 'healthArchive':
return const HealthArchivePage();
case 'followups':

View File

@@ -2,65 +2,84 @@ import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'app_colors.dart';
/// 健康管家 — 清爽紫蓝风
/// 健康管家 —— 蚂蚁阿福风格:紫蓝渐变 + 超大圆角 + 漂浮卡片
class AppTheme {
AppTheme._();
// ── 主色调 ──
static const Color primary = AppColors.primaryPurple;
static const Color primaryLight = AppColors.iconBackground;
static const Color primaryDark = AppColors.iconColor;
static const Color primaryGlow = AppColors.purpleGradientStart;
static const Color primary = AppColors.primary;
static const Color primaryLight = AppColors.primaryLight;
static const Color primaryDark = AppColors.primaryDark;
static const Color primaryMid = AppColors.primaryMid;
static const Color accent = AppColors.accentCyan;
static const Color info = AppColors.accentCyan;
// ── 中性色 ──
static const Color bg = AppColors.backgroundSecondary;
static const Color surface = AppColors.cardBackground;
static const Color text = AppColors.textPrimary;
static const Color textSub = AppColors.textSecondary;
static const Color bg = AppColors.background;
static const Color bgSoft = AppColors.backgroundSoft;
static const Color surface = AppColors.cardBackground;
static const Color surfaceInner = AppColors.cardInner;
static const Color text = AppColors.textPrimary;
static const Color textSub = AppColors.textSecondary;
static const Color textHint = AppColors.textHint;
static const Color border = AppColors.border;
static const Color divider = AppColors.borderLight;
static const Color border = AppColors.border;
static const Color divider = AppColors.borderLight;
// ── 语义色 ──
static const Color success = AppColors.success;
static const Color error = AppColors.error;
static const Color successLight = AppColors.successLight;
static const Color error = AppColors.error;
static const Color errorLight = AppColors.errorLight;
static const Color warning = AppColors.warning;
static const Color accent = AppColors.primaryBlue;
static const Color info = AppColors.primaryBlue;
static const Color warningLight = AppColors.warningLight;
// ── 圆角 ──
static const double rXs = 6;
static const double rSm = 10;
static const double rMd = 14;
static const double rLg = 20;
static const double rXl = 28;
// ── 圆角(阿福风格:超大圆角)──
static const double rXs = 8;
static const double rSm = 12;
static const double rMd = 18;
static const double rLg = 24;
static const double rXl = 32;
static const double rPill = 999;
// ── 间距 ──
static const double sXs = 4;
static const double sSm = 8;
static const double sMd = 12;
static const double sLg = 16;
static const double sXl = 20;
static const double sXs = 4;
static const double sSm = 8;
static const double sMd = 12;
static const double sLg = 16;
static const double sXl = 20;
static const double sXxl = 28;
// ── 阴影 ──
static BoxShadow get shadowCard =>
BoxShadow(color: primary.withAlpha(14), blurRadius: 20, offset: const Offset(0, 4));
static BoxShadow get shadowLight =>
BoxShadow(color: primary.withAlpha(8), blurRadius: 12, offset: const Offset(0, 2));
static BoxShadow get shadowElevated =>
BoxShadow(color: primary.withAlpha(20), blurRadius: 30, offset: const Offset(0, 8));
// ── 阴影(淡紫柔和阴影)──
static BoxShadow get shadowCard => BoxShadow(
color: AppColors.primary.withOpacity(0.08),
blurRadius: 24,
offset: const Offset(0, 8),
);
static BoxShadow get shadowLight => BoxShadow(
color: AppColors.primary.withOpacity(0.05),
blurRadius: 16,
offset: const Offset(0, 4),
);
static BoxShadow get shadowElevated => BoxShadow(
color: AppColors.primary.withOpacity(0.15),
blurRadius: 32,
offset: const Offset(0, 10),
);
static BoxShadow get shadowButton => BoxShadow(
color: AppColors.primary.withOpacity(0.25),
blurRadius: 16,
offset: const Offset(0, 6),
);
// ── 智能体配色 ──
static const Map<String, Color> agentColors = {
'default': AppColors.iconBackground,
'consultation': Color(0xFFE0F0FF),
'health': Color(0xFFE8FFE8),
'diet': Color(0xFFFFF0E0),
'medication': Color(0xFFFFE8EC),
'report': Color(0xFFE8F0FF),
'exercise': Color(0xFFF0E8FF),
'default': AppColors.primaryLight,
'consultation': Color(0xFFE0E7FF),
'health': Color(0xFFDCFCE7),
'diet': Color(0xFFFEF3C7),
'medication': Color(0xFFFCE7F3),
'report': Color(0xFFDBEAFE),
'exercise': Color(0xFFEDE9FE),
};
static Color agentLight(String? name) => agentColors[name] ?? primaryLight;
@@ -71,19 +90,26 @@ class AppTheme {
colorScheme: ColorScheme.fromSeed(
seedColor: primary,
primary: primary,
surface: bg,
primaryContainer: primaryLight,
onPrimary: Colors.white,
secondary: accent,
surface: surface,
background: bg,
brightness: Brightness.light,
),
scaffoldBackgroundColor: bg,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: primaryLight.withOpacity(0.5),
fontFamily: null,
appBarTheme: const AppBarTheme(
backgroundColor: surface,
appBarTheme: AppBarTheme(
backgroundColor: bg,
foregroundColor: text,
elevation: 0,
centerTitle: true,
centerTitle: false,
scrolledUnderElevation: 0,
titleTextStyle: TextStyle(fontSize: 23, fontWeight: FontWeight.w600, color: text),
titleTextStyle: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: text),
),
cardTheme: CardThemeData(
@@ -95,7 +121,7 @@ class AppTheme {
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: bg,
fillColor: bgSoft,
contentPadding: const EdgeInsets.symmetric(horizontal: sLg, vertical: 16),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(rMd), borderSide: BorderSide.none),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(rMd), borderSide: BorderSide.none),
@@ -103,7 +129,7 @@ class AppTheme {
borderRadius: BorderRadius.circular(rMd),
borderSide: const BorderSide(color: primary, width: 2),
),
hintStyle: const TextStyle(color: textHint, fontSize: 20),
hintStyle: TextStyle(color: textHint, fontSize: 16),
),
elevatedButtonTheme: ElevatedButtonThemeData(
@@ -112,28 +138,29 @@ class AppTheme {
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rPill)),
textStyle: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600),
textStyle: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
elevation: 0,
shadowColor: primary.withOpacity(0.3),
),
),
dialogTheme: DialogThemeData(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rXl))),
textTheme: const TextTheme(
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: text, letterSpacing: -0.5),
titleLarge: TextStyle(fontSize: 23, fontWeight: FontWeight.w600, color: text),
titleMedium: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: text),
bodyLarge: TextStyle(fontSize: 21, color: text, height: 1.5),
bodyMedium: TextStyle(fontSize: 20, color: textSub, height: 1.4),
labelMedium: TextStyle(fontSize: 18, color: textSub),
labelSmall: TextStyle(fontSize: 16, color: textHint),
headlineLarge: TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: text, letterSpacing: -0.3),
titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: text),
titleMedium: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: text),
bodyLarge: TextStyle(fontSize: 16, color: text, height: 1.5),
bodyMedium: TextStyle(fontSize: 14, color: textSub, height: 1.4),
labelMedium: TextStyle(fontSize: 13, color: textSub),
labelSmall: TextStyle(fontSize: 12, color: textHint),
),
);
// ── Shadcn UI 主题 ──
// ── Shadcn UI 主题(紫蓝系) ──
static ShadThemeData get shadTheme => ShadThemeData(
brightness: Brightness.light,
colorScheme: const ShadVioletColorScheme.light(
colorScheme: ShadVioletColorScheme.light(
background: bg,
foreground: text,
card: surface,
@@ -142,9 +169,9 @@ class AppTheme {
popoverForeground: text,
primary: primary,
primaryForeground: Colors.white,
secondary: divider,
secondaryForeground: text,
muted: divider,
secondary: primaryLight,
secondaryForeground: primaryDark,
muted: bgSoft,
mutedForeground: textSub,
accent: accent,
accentForeground: Colors.white,
@@ -153,7 +180,7 @@ class AppTheme {
border: border,
input: border,
ring: primary,
selection: primaryGlow,
selection: primaryLight,
),
radius: BorderRadius.circular(rMd),
);