feat: 蓝牙血压计基础架构 + UI配色体系升级
- 新增 AppColors 配色体系(紫蓝渐变 + 超大圆角 + 漂浮卡片) - 新增 OmronBleService(BLE 扫描/连接/SFLOAT 协议解析) - 新增 BpReading 数据模型 + 血压数据自动同步后端 - 新增 DeviceScanPage(血压计扫描绑定页)+ DeviceBindPage(已绑定管理页) - 侧边栏新增"蓝牙设备"入口,ProfilePage 移除设备管理 - Android/iOS BLE 权限配置(BLUETOOTH_SCAN/CONNECT) - AppTheme 升级:新阴影系统、新语义色、圆角放大
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════ 自定义滑动删除组件 ═══════════════════
|
||||
|
||||
Reference in New Issue
Block a user