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,10 +1,12 @@
import 'package:flutter/material.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';
import '../providers/auth_provider.dart';
import '../providers/data_providers.dart';
import '../providers/omron_device_provider.dart';
/// 饮食记录列表(左滑删除)
class DietRecordListPage extends ConsumerStatefulWidget {
@@ -783,10 +785,214 @@ class StaticTextPage extends ConsumerWidget {
}
}
/// 设备管理(占位
/// 血压计设备管理(已绑定/未绑定
class DeviceManagementPage extends ConsumerWidget {
const DeviceManagementPage({super.key});
@override Widget build(BuildContext context, WidgetRef ref) => _empty(context, '设备管理', '暂无绑定设备');
@override Widget build(BuildContext context, WidgetRef ref) {
final device = ref.watch(omronDeviceProvider);
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.cardBackground,
title: const Text('蓝牙血压计'),
),
body: device.isBound ? _buildBoundCard(context, ref, device) : _buildEmptyState(context, ref),
);
}
Widget _buildEmptyState(BuildContext context, WidgetRef ref) => Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Container(
width: 88, height: 88,
decoration: BoxDecoration(
color: AppColors.iconBackground,
borderRadius: BorderRadius.circular(28),
),
child: Icon(Icons.bluetooth_disabled, size: 44, color: AppColors.textHint),
),
const SizedBox(height: 20),
const Text('未绑定设备', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
const SizedBox(height: 8),
const Text('连接欧姆龙血压计,自动同步测量数据', style: TextStyle(fontSize: 15, color: AppColors.textHint)),
const SizedBox(height: 28),
SizedBox(
width: 220,
height: 52,
child: ElevatedButton.icon(
onPressed: () => pushRoute(ref, 'deviceScan'),
icon: const Icon(Icons.add, size: 22),
label: const Text('添加设备', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.iconColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
),
),
]),
),
);
Widget _buildBoundCard(BuildContext context, WidgetRef ref, DeviceBindState device) => SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(children: [
// 设备信息卡片
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.cardBackground,
borderRadius: BorderRadius.circular(20),
boxShadow: AppColors.cardShadow,
),
child: Column(children: [
Container(
width: 72, height: 72,
decoration: BoxDecoration(
gradient: AppColors.purpleBlueGradient,
borderRadius: BorderRadius.circular(22),
),
child: const Icon(Icons.bluetooth_connected, size: 36, color: Colors.white),
),
const SizedBox(height: 16),
Text(device.name ?? '血压计', style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
const SizedBox(height: 6),
Text('MAC: ${device.mac ?? ''}', style: const TextStyle(fontSize: 14, color: AppColors.textHint)),
if (device.lastSync != null) ...[
const SizedBox(height: 4),
Text('上次同步: ${device.lastSync}', style: const TextStyle(fontSize: 13, color: AppColors.textHint)),
],
const SizedBox(height: 24),
Row(children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => _unbind(context, ref),
icon: const Icon(Icons.delete_outline, size: 18, color: AppColors.error),
label: const Text('解绑', style: TextStyle(color: AppColors.error)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: AppColors.error),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () => pushRoute(ref, 'deviceScan'),
icon: const Icon(Icons.swap_horiz, size: 18),
label: const Text('更换设备'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.iconColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
),
]),
]),
),
// 最后读数卡片
if (device.lastReading != null) ...[
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.cardBackground,
borderRadius: BorderRadius.circular(20),
boxShadow: AppColors.cardShadow,
),
child: Column(children: [
Row(children: [
Container(
width: 6, height: 18,
decoration: BoxDecoration(
gradient: AppColors.purpleBlueGradient,
borderRadius: BorderRadius.circular(3),
),
),
const SizedBox(width: 8),
const Text('最近一次测量', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
]),
const SizedBox(height: 16),
Row(children: [
Container(
width: 56, height: 56,
decoration: BoxDecoration(
gradient: AppColors.lightGradient,
borderRadius: BorderRadius.circular(16),
),
child: const Text('🩺', style: TextStyle(fontSize: 28)),
),
const SizedBox(width: 16),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(device.lastReading!.display, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w800, color: AppColors.textPrimary, letterSpacing: -0.5)),
Text('mmHg', style: TextStyle(fontSize: 14, color: AppColors.textHint)),
]),
const Spacer(),
if (device.lastReading!.pulse != null)
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
Text('${device.lastReading!.pulse}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: AppColors.iconColor)),
const Text('bpm', style: TextStyle(fontSize: 13, color: AppColors.textHint)),
]),
]),
]),
),
],
// 使用说明
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.iconBackground,
borderRadius: BorderRadius.circular(16),
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Icon(Icons.lightbulb_outline, size: 18, color: AppColors.iconColor),
const SizedBox(width: 8),
const Text('使用说明', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
]),
const SizedBox(height: 10),
_guideItem('1', '血压计装好电池,绑好袖带'),
_guideItem('2', '血压计按开始键开始测量'),
_guideItem('3', '测量完成后数据自动同步到 App'),
_guideItem('4', '打开此页面时保持蓝牙开启'),
]),
),
]),
);
Widget _guideItem(String num, String text) => Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('$num.', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.iconColor)),
const SizedBox(width: 6),
Expanded(child: Text(text, style: const TextStyle(fontSize: 14, color: AppColors.textSecondary))),
]),
);
void _unbind(BuildContext context, WidgetRef ref) async {
final ok = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog(
title: 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))),
],
));
if (ok == true) {
await ref.read(omronDeviceProvider.notifier).unbind();
}
}
}
// ═══════════════════ 自定义滑动删除组件 ═══════════════════