feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
This commit is contained in:
@@ -5,7 +5,6 @@ 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';
|
||||
import '../widgets/common_widgets.dart';
|
||||
|
||||
/// 饮食记录列表(趋势+日历+胶囊详情+侧滑编辑删除)
|
||||
@@ -29,17 +28,19 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
|
||||
Future<void> _refresh() async {
|
||||
final records = await ref.read(dietServiceProvider).getRecords();
|
||||
if (mounted)
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_data = records;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _delete(String id) async {
|
||||
await ref.read(dietServiceProvider).deleteRecord(id);
|
||||
if (mounted)
|
||||
if (mounted) {
|
||||
setState(() => _data.removeWhere((d) => d['id']?.toString() == id));
|
||||
}
|
||||
}
|
||||
|
||||
String _dateKey(DateTime d) =>
|
||||
@@ -67,7 +68,7 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_loading)
|
||||
if (_loading) {
|
||||
return GradientScaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
@@ -84,6 +85,7 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final todayRecords = _dayRecords(_selectedDate);
|
||||
final totalCal = todayRecords.fold(
|
||||
@@ -441,15 +443,17 @@ class _TrendChart extends StatelessWidget {
|
||||
const _TrendChart(this.data);
|
||||
@override
|
||||
Widget build(BuildContext c) {
|
||||
if (data.isEmpty)
|
||||
if (data.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('暂无数据', style: TextStyle(color: AppColors.textHint)),
|
||||
);
|
||||
}
|
||||
final m = data.reduce((a, b) => a > b ? a : b);
|
||||
if (m == 0)
|
||||
if (m == 0) {
|
||||
return const Center(
|
||||
child: Text('暂无数据', style: TextStyle(color: AppColors.textHint)),
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: List.generate(data.length, (i) {
|
||||
@@ -549,10 +553,11 @@ class DietRecordDetailPage extends ConsumerWidget {
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: service.getRecords(),
|
||||
builder: (ctx, snap) {
|
||||
if (!snap.hasData)
|
||||
if (!snap.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppTheme.primary),
|
||||
);
|
||||
}
|
||||
final d = snap.data!.firstWhere(
|
||||
(r) => r['id']?.toString() == id,
|
||||
orElse: () => <String, dynamic>{},
|
||||
@@ -734,11 +739,12 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
|
||||
final done = items
|
||||
.where((it) => it['isCompleted'] == true)
|
||||
.length;
|
||||
final weekStart = p['weekStartDate']?.toString() ?? '';
|
||||
// 用 DayOfWeek 匹配今天(C#: 0=Sun, 6=Sat; Dart weekday%7 对齐)
|
||||
final todayCsDow = DateTime.now().weekday % 7;
|
||||
final startDate = p['startDate']?.toString() ?? '';
|
||||
final endDate = p['endDate']?.toString() ?? '';
|
||||
final now = DateTime.now();
|
||||
final todayKey = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
final todayItem = items.cast<Map<String, dynamic>>().firstWhere(
|
||||
(it) => it['dayOfWeek'] == todayCsDow,
|
||||
(it) => it['scheduledDate']?.toString() == todayKey,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
final hasTodayItem = todayItem.isNotEmpty;
|
||||
@@ -786,7 +792,7 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'$weekStart 起 · ${items.first['durationMinutes'] ?? 0}分钟/天',
|
||||
'$startDate 至 $endDate · ${items.first['durationMinutes'] ?? 0}分钟/天',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
@@ -855,12 +861,14 @@ class _ExercisePlanCreatePageState
|
||||
extends ConsumerState<ExercisePlanCreatePage> {
|
||||
final _nameCtrl = TextEditingController();
|
||||
final _durationCtrl = TextEditingController(text: '30');
|
||||
final _daysCtrl = TextEditingController(text: '7');
|
||||
DateTime _start = DateTime.now();
|
||||
DateTime _end = DateTime.now().add(const Duration(days: 6));
|
||||
TimeOfDay _reminderTime = const TimeOfDay(hour: 19, minute: 0);
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_durationCtrl.dispose();
|
||||
_daysCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -873,13 +881,16 @@ class _ExercisePlanCreatePageState
|
||||
return;
|
||||
}
|
||||
final dur = int.tryParse(_durationCtrl.text) ?? 30;
|
||||
final days = (int.tryParse(_daysCtrl.text) ?? 7).clamp(1, 366);
|
||||
final end = _start.add(Duration(days: days - 1));
|
||||
await ref.read(exerciseServiceProvider).createPlanSimple({
|
||||
'exerciseType': name,
|
||||
'durationMinutes': dur,
|
||||
'startDate':
|
||||
'${_start.year}-${_start.month.toString().padLeft(2, '0')}-${_start.day.toString().padLeft(2, '0')}',
|
||||
'endDate':
|
||||
'${_end.year}-${_end.month.toString().padLeft(2, '0')}-${_end.day.toString().padLeft(2, '0')}',
|
||||
'${end.year}-${end.month.toString().padLeft(2, '0')}-${end.day.toString().padLeft(2, '0')}',
|
||||
'reminderTime': '${_reminderTime.hour.toString().padLeft(2, '0')}:${_reminderTime.minute.toString().padLeft(2, '0')}',
|
||||
});
|
||||
ref.invalidate(currentExercisePlanProvider);
|
||||
if (mounted) {
|
||||
@@ -904,9 +915,11 @@ class _ExercisePlanCreatePageState
|
||||
const SizedBox(height: 16),
|
||||
_field('每天时长(分钟)', _durationCtrl, hint: '30', number: true),
|
||||
const SizedBox(height: 16),
|
||||
_field('坚持天数', _daysCtrl, hint: '7', number: true),
|
||||
const SizedBox(height: 16),
|
||||
_dateField('开始日期', _start, (d) => setState(() => _start = d)),
|
||||
const SizedBox(height: 16),
|
||||
_dateField('结束日期', _end, (d) => setState(() => _end = d)),
|
||||
_timeField(),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -1006,6 +1019,29 @@ class _ExercisePlanCreatePageState
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _timeField() {
|
||||
final value = '${_reminderTime.hour.toString().padLeft(2, '0')}:${_reminderTime.minute.toString().padLeft(2, '0')}';
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('每日提醒时间', style: TextStyle(fontSize: 17, color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 6),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final picked = await showTimePicker(context: context, initialTime: _reminderTime);
|
||||
if (picked != null && mounted) setState(() => _reminderTime = picked);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(value, style: const TextStyle(fontSize: 19)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 复查列表(从服务器读取)
|
||||
@@ -1254,8 +1290,9 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
|
||||
_diagnosisCtrl.text = a['diagnosis'] ?? '';
|
||||
final st = a['surgeryType'] as String?;
|
||||
final sd = a['surgeryDate'] as String?;
|
||||
if (st != null && st.isNotEmpty)
|
||||
if (st != null && st.isNotEmpty) {
|
||||
_surgeries.add({'type': st, 'date': sd ?? ''});
|
||||
}
|
||||
_allergiesCtrl.text = (a['allergies'] as List?)?.join('、') ?? '';
|
||||
_chronicCtrl.text = (a['chronicDiseases'] as List?)?.join('、') ?? '';
|
||||
_dietCtrl.text = (a['dietRestrictions'] as List?)?.join('、') ?? '';
|
||||
@@ -1839,19 +1876,16 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||||
'/api/calendar/day',
|
||||
queryParameters: {'date': _dateKey(d)},
|
||||
);
|
||||
if (mounted)
|
||||
if (mounted) {
|
||||
setState(
|
||||
() => _selectedDayData = res.data['data'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final today = DateTime.now();
|
||||
final isToday =
|
||||
_selectedDate != null && _dateKey(_selectedDate!) == _dateKey(today);
|
||||
|
||||
return GradientScaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
@@ -2053,13 +2087,14 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||||
};
|
||||
|
||||
Widget _buildDayDetail() {
|
||||
if (_selectedDate == null)
|
||||
if (_selectedDate == null) {
|
||||
return const Center(
|
||||
child: Text('点击日期查看当天安排', style: TextStyle(color: AppColors.textHint)),
|
||||
);
|
||||
}
|
||||
if (_selectedDayData == null) {
|
||||
final evs = _events[_dateKey(_selectedDate!)] ?? [];
|
||||
if (evs.isEmpty)
|
||||
if (evs.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -2077,10 +2112,11 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final data = _selectedDayData;
|
||||
if (data == null)
|
||||
if (data == null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -2094,6 +2130,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final meds =
|
||||
(data['medications'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
@@ -2359,437 +2396,6 @@ class StaticTextPage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 旧版设备管理页,已移至 device/device_management_page.dart
|
||||
class _DeviceManagementPageLegacy extends ConsumerWidget {
|
||||
const _DeviceManagementPageLegacy({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final device = ref.watch(omronDeviceProvider);
|
||||
return GradientScaffold(
|
||||
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.iconBg,
|
||||
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.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
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(AppTheme.rMd),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
gradient: device.isConnected
|
||||
? AppColors.primaryGradient
|
||||
: const LinearGradient(
|
||||
colors: [Color(0xFF9CA3AF), Color(0xFF6B7280)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
child: Icon(
|
||||
device.isConnected
|
||||
? Icons.bluetooth_connected
|
||||
: Icons.bluetooth_disabled,
|
||||
size: 36,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: device.isConnected
|
||||
? const Color(0xFFD1FAE5)
|
||||
: const Color(0xFFFEE2E2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
device.isConnected ? '已连接' : '未连接',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: device.isConnected
|
||||
? const Color(0xFF059669)
|
||||
: const Color(0xFFDC2626),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
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),
|
||||
if (device.isConnected) ...[
|
||||
// 已连接:显示断开和解绑
|
||||
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(AppTheme.rMd),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
// 未连接:显示重新连接和解绑
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => pushRoute(ref, 'deviceScan'),
|
||||
icon: const Icon(Icons.bluetooth_searching, size: 20),
|
||||
label: const Text(
|
||||
'重新连接设备',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
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(AppTheme.rMd),
|
||||
),
|
||||
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(AppTheme.rMd),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.primaryGradient,
|
||||
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(
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
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.primary,
|
||||
),
|
||||
),
|
||||
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.iconBg,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lightbulb_outline,
|
||||
size: 18,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
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.primary,
|
||||
),
|
||||
),
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _empty(BuildContext context, String title, String subtitle) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
Reference in New Issue
Block a user