Commit 9956ee47f347c3fda0b5a9c8407b5dc3d6e413ea

Authored by 常守达
1 parent 2166905b

feat(today): 苹果登陆

@@ -3,15 +3,15 @@ @@ -3,15 +3,15 @@
3 archiveVersion = 1; 3 archiveVersion = 1;
4 classes = { 4 classes = {
5 }; 5 };
6 - objectVersion = 77; 6 + objectVersion = 54;
7 objects = { 7 objects = {
8 8
9 /* Begin PBXBuildFile section */ 9 /* Begin PBXBuildFile section */
10 0798E27DB95F2A823881D75A /* Pods_Runner_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A488EFC0BD642ED604C4E43 /* Pods_Runner_Watch_App.framework */; }; 10 0798E27DB95F2A823881D75A /* Pods_Runner_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A488EFC0BD642ED604C4E43 /* Pods_Runner_Watch_App.framework */; };
11 20A3C4994C9B0F60D1F3BCBE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E8DC48B3736B7FEB548D0E65 /* Pods_Runner.framework */; }; 11 20A3C4994C9B0F60D1F3BCBE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E8DC48B3736B7FEB548D0E65 /* Pods_Runner.framework */; };
12 - 66FB19002FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19032FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift */; };  
13 - 66FB19012FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19042FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift */; };  
14 - 66FB19022FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19052FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift */; }; 12 + 66FB19002FDBC80100F515B4 /* DFWatchFigmaTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19032FDBC80100F515B4 /* DFWatchFigmaTokens.swift */; };
  13 + 66FB19012FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19042FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift */; };
  14 + 66FB19022FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19052FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift */; };
15 66FBE58C2FDA4F0F00F515B4 /* Runner Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 66FBE58B2FDA4F0F00F515B4 /* Runner Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 15 66FBE58C2FDA4F0F00F515B4 /* Runner Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 66FBE58B2FDA4F0F00F515B4 /* Runner Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
16 /* End PBXBuildFile section */ 16 /* End PBXBuildFile section */
17 17
@@ -54,9 +54,9 @@ @@ -54,9 +54,9 @@
54 27E4D5A8995AB1EC91E7330E /* Pods-Runner Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner Watch App.debug.xcconfig"; path = "Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App.debug.xcconfig"; sourceTree = "<group>"; }; 54 27E4D5A8995AB1EC91E7330E /* Pods-Runner Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner Watch App.debug.xcconfig"; path = "Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App.debug.xcconfig"; sourceTree = "<group>"; };
55 4A9B7C102FDB0A1200F515B4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; 55 4A9B7C102FDB0A1200F515B4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
56 4A9B7C112FDB0A1200F515B4 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; 56 4A9B7C112FDB0A1200F515B4 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
57 - 66FB19032FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift"; sourceTree = "<group>"; };  
58 - 66FB19042FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift"; sourceTree = "<group>"; };  
59 - 66FB19052FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift"; sourceTree = "<group>"; }; 57 + 66FB19032FDBC80100F515B4 /* DFWatchFigmaTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift"; sourceTree = "<group>"; };
  58 + 66FB19042FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift"; sourceTree = "<group>"; };
  59 + 66FB19052FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift"; sourceTree = "<group>"; };
60 66FBE57E2FDA4F0E00F515B4 /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 60 66FBE57E2FDA4F0E00F515B4 /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
61 66FBE58B2FDA4F0F00F515B4 /* Runner Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Runner Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 61 66FBE58B2FDA4F0F00F515B4 /* Runner Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Runner Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
62 66FBE5A72FDA518C00F515B4 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 62 66FBE5A72FDA518C00F515B4 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
@@ -97,6 +97,8 @@ @@ -97,6 +97,8 @@
97 }; 97 };
98 66FBE58F2FDA4F0F00F515B4 /* Runner Watch App */ = { 98 66FBE58F2FDA4F0F00F515B4 /* Runner Watch App */ = {
99 isa = PBXFileSystemSynchronizedRootGroup; 99 isa = PBXFileSystemSynchronizedRootGroup;
  100 + exceptions = (
  101 + );
100 path = "Runner Watch App"; 102 path = "Runner Watch App";
101 sourceTree = "<group>"; 103 sourceTree = "<group>";
102 }; 104 };
@@ -134,9 +136,9 @@ @@ -134,9 +136,9 @@
134 66FB19072FDBC80100F515B4 /* FigmaHome Preview Sources */ = { 136 66FB19072FDBC80100F515B4 /* FigmaHome Preview Sources */ = {
135 isa = PBXGroup; 137 isa = PBXGroup;
136 children = ( 138 children = (
137 - 66FB19052FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift */,  
138 - 66FB19042FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift */,  
139 - 66FB19032FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift */, 139 + 66FB19052FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift */,
  140 + 66FB19042FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift */,
  141 + 66FB19032FDBC80100F515B4 /* DFWatchFigmaTokens.swift */,
140 ); 142 );
141 name = "FigmaHome Preview Sources"; 143 name = "FigmaHome Preview Sources";
142 sourceTree = "<group>"; 144 sourceTree = "<group>";
@@ -303,14 +305,10 @@ @@ -303,14 +305,10 @@
303 inputFileListPaths = ( 305 inputFileListPaths = (
304 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", 306 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
305 ); 307 );
306 - inputPaths = (  
307 - );  
308 name = "[CP] Copy Pods Resources"; 308 name = "[CP] Copy Pods Resources";
309 outputFileListPaths = ( 309 outputFileListPaths = (
310 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", 310 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
311 ); 311 );
312 - outputPaths = (  
313 - );  
314 runOnlyForDeploymentPostprocessing = 0; 312 runOnlyForDeploymentPostprocessing = 0;
315 shellPath = /bin/sh; 313 shellPath = /bin/sh;
316 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; 314 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -381,14 +379,10 @@ @@ -381,14 +379,10 @@
381 inputFileListPaths = ( 379 inputFileListPaths = (
382 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 380 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
383 ); 381 );
384 - inputPaths = (  
385 - );  
386 name = "[CP] Embed Pods Frameworks"; 382 name = "[CP] Embed Pods Frameworks";
387 outputFileListPaths = ( 383 outputFileListPaths = (
388 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 384 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
389 ); 385 );
390 - outputPaths = (  
391 - );  
392 runOnlyForDeploymentPostprocessing = 0; 386 runOnlyForDeploymentPostprocessing = 0;
393 shellPath = /bin/sh; 387 shellPath = /bin/sh;
394 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 388 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -402,14 +396,10 @@ @@ -402,14 +396,10 @@
402 inputFileListPaths = ( 396 inputFileListPaths = (
403 "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist", 397 "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
404 ); 398 );
405 - inputPaths = (  
406 - );  
407 name = "[CP] Embed Pods Frameworks"; 399 name = "[CP] Embed Pods Frameworks";
408 outputFileListPaths = ( 400 outputFileListPaths = (
409 "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist", 401 "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
410 ); 402 );
411 - outputPaths = (  
412 - );  
413 runOnlyForDeploymentPostprocessing = 0; 403 runOnlyForDeploymentPostprocessing = 0;
414 shellPath = /bin/sh; 404 shellPath = /bin/sh;
415 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks.sh\"\n"; 405 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks.sh\"\n";
@@ -451,9 +441,9 @@ @@ -451,9 +441,9 @@
451 isa = PBXSourcesBuildPhase; 441 isa = PBXSourcesBuildPhase;
452 buildActionMask = 2147483647; 442 buildActionMask = 2147483647;
453 files = ( 443 files = (
454 - 66FB19022FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift in Sources */,  
455 - 66FB19012FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift in Sources */,  
456 - 66FB19002FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift in Sources */, 444 + 66FB19022FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift in Sources */,
  445 + 66FB19012FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift in Sources */,
  446 + 66FB19002FDBC80100F515B4 /* DFWatchFigmaTokens.swift in Sources */,
457 ); 447 );
458 runOnlyForDeploymentPostprocessing = 0; 448 runOnlyForDeploymentPostprocessing = 0;
459 }; 449 };
@@ -28,7 +28,8 @@ class BindPartnerController extends GetxController { @@ -28,7 +28,8 @@ class BindPartnerController extends GetxController {
28 }); 28 });
29 29
30 final userPrefs = Get.find<UserPreferencesStorage>(); 30 final userPrefs = Get.find<UserPreferencesStorage>();
31 - myInviteCode.value = userPrefs.preferences.value.meUserInfo?.pairCode ?? ''; 31 + myInviteCode.value =
  32 + userPrefs.preferences.value.meUserInfo?.uniqueCode ?? '';
32 } 33 }
33 34
34 @override 35 @override
1 import 'dart:async'; 1 import 'dart:async';
  2 +import 'dart:math';
2 3
3 import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart'; 4 import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
  5 +import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart';
  6 +import 'package:doublefeel_flutter/app/modules/home/controllers/home_controller.dart';
  7 +import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_controller.dart';
4 import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; 8 import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart';
  9 +import 'package:doublefeel_flutter/app/routes/app_pages.dart';
5 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 10 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
6 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 11 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
7 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 12 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
@@ -10,7 +15,6 @@ import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart' @@ -10,7 +15,6 @@ import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'
10 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 15 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
11 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; 16 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
12 import 'package:doublefeel_flutter/data/models/health/health_models.dart'; 17 import 'package:doublefeel_flutter/data/models/health/health_models.dart';
13 -import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart';  
14 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; 18 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
15 import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; 19 import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
16 import 'package:flutter/material.dart'; 20 import 'package:flutter/material.dart';
@@ -75,12 +79,6 @@ class TodayController extends GetxController { @@ -75,12 +79,6 @@ class TodayController extends GetxController {
75 final showHealthDataAuthCard = true.obs; 79 final showHealthDataAuthCard = true.obs;
76 final stressSubtitle = 'Hi, 你今日的综合压力状态'.obs; 80 final stressSubtitle = 'Hi, 你今日的综合压力状态'.obs;
77 81
78 - // ── 压力状态 ─────────────────────────────  
79 - final stressLabel = '状态正常'.obs;  
80 -  
81 - /// 进度条压力分(null=加载中, 0~100=压力分: 0优秀, 100过载)  
82 - final stressScore = Rxn<int>();  
83 -  
84 // ── HRV 表盘引导 Banner ─────────────────── 82 // ── HRV 表盘引导 Banner ───────────────────
85 final showHrvAdBanner = true.obs; 83 final showHrvAdBanner = true.obs;
86 final showPartnerAdBanner = true.obs; 84 final showPartnerAdBanner = true.obs;
@@ -93,13 +91,6 @@ class TodayController extends GetxController { @@ -93,13 +91,6 @@ class TodayController extends GetxController {
93 final avgHrv = '--'.obs; 91 final avgHrv = '--'.obs;
94 final restingHeartRate = '--'.obs; 92 final restingHeartRate = '--'.obs;
95 93
96 - // ── 睡眠卡片 ──────────────────────────────  
97 - final sleepHours = '--'.obs;  
98 - final sleepMinutes = '--'.obs;  
99 - final sleepQuality = '--'.obs;  
100 - final sleepAverageHeartRate = '--'.obs;  
101 - final sleepProgress = 0.0.obs;  
102 -  
103 // ── 健身卡片 ────────────────────────────── 94 // ── 健身卡片 ──────────────────────────────
104 final activityCalories = '--'.obs; 95 final activityCalories = '--'.obs;
105 final activityExerciseMinutes = '--'.obs; 96 final activityExerciseMinutes = '--'.obs;
@@ -219,7 +210,7 @@ class TodayController extends GetxController { @@ -219,7 +210,7 @@ class TodayController extends GetxController {
219 210
220 Future<void> _refreshHealthDataForDate(DateTime date) async { 211 Future<void> _refreshHealthDataForDate(DateTime date) async {
221 _clearRealTimeData(); 212 _clearRealTimeData();
222 - final intDate = int.parse(DateFormat('yyyyMM01').format(date)); 213 + final intDate = int.parse(DateFormat('yyyyMMdd').format(date));
223 214
224 final isToday = DateUtils.isSameDay(date, lastSelectableDay); 215 final isToday = DateUtils.isSameDay(date, lastSelectableDay);
225 stressSubtitle.value = isToday ? 'Hi, 你今日的综合压力状态' : '该日压力状态'; 216 stressSubtitle.value = isToday ? 'Hi, 你今日的综合压力状态' : '该日压力状态';
@@ -261,11 +252,7 @@ class TodayController extends GetxController { @@ -261,11 +252,7 @@ class TodayController extends GetxController {
261 252
262 void _clearRealTimeData() { 253 void _clearRealTimeData() {
263 restingHeartRate.value = '--'; 254 restingHeartRate.value = '--';
264 - sleepAverageHeartRate.value = '--';  
265 - sleepHours.value = '--';  
266 - sleepMinutes.value = '--';  
267 - sleepQuality.value = '--';  
268 - sleepProgress.value = 0.0; 255 +
269 activityCalories.value = '--'; 256 activityCalories.value = '--';
270 activityExerciseMinutes.value = '--'; 257 activityExerciseMinutes.value = '--';
271 activityStandHours.value = '--'; 258 activityStandHours.value = '--';
@@ -276,69 +263,6 @@ class TodayController extends GetxController { @@ -276,69 +263,6 @@ class TodayController extends GetxController {
276 stressChartData.clear(); 263 stressChartData.clear();
277 hrvAnnotations.clear(); 264 hrvAnnotations.clear();
278 avgHrv.value = '--'; 265 avgHrv.value = '--';
279 - stressLabel.value = '状态正常';  
280 - stressScore.value = null;  
281 - }  
282 -  
283 - void _applyTodayData(TodayStatusData today) {  
284 - final recent = today.recentData;  
285 - if (recent?.heartRate != null) {  
286 - restingHeartRate.value = _formatMetric(recent!.heartRate);  
287 - if (sleepAverageHeartRate.value == '--') {  
288 - sleepAverageHeartRate.value = _formatMetric(recent.heartRate);  
289 - }  
290 - }  
291 -  
292 - _applySleepDuration(today.sleepDuration);  
293 - _applyActivityRecentData(recent);  
294 -  
295 - final hrvPoints = _buildHrvChartData(today.hrvDataList);  
296 - hrvChartData.assignAll(hrvPoints);  
297 - stressChartData.assignAll(  
298 - hrvPoints.map((point) {  
299 - return HrvDataPoint(  
300 - hour: point.hour,  
301 - hrv: _stressScoreFromHrv(point.hrv),  
302 - );  
303 - }),  
304 - );  
305 -  
306 - final latest = _latestTodayHrv(today.hrvDataList);  
307 - _applyLatestTodayHrv(latest);  
308 -  
309 - final hrvValues = hrvPoints.map((point) => point.hrv).toList();  
310 - if (hrvValues.isNotEmpty) {  
311 - final average = hrvValues.reduce((value, element) => value + element) /  
312 - hrvValues.length;  
313 - avgHrv.value = _formatMetric(average);  
314 -  
315 - final latestStatus = latest?.hrvStatus;  
316 - if (latestStatus != null) {  
317 - stressLabel.value = latestStatus.title;  
318 - }  
319 -  
320 - // 更新进度条压力分  
321 - final latestHrvVal = latest?.value ?? average;  
322 - stressScore.value = null; // _computeStressScore(latestHrvVal);  
323 -  
324 - _updateLatestHrvAnnotation(latest);  
325 - }  
326 - }  
327 -  
328 - void _applyLatestHrvData(LatestHrvData latestHrv) {  
329 - final userHrv = latestHrv.userHrv;  
330 - if (avgHrv.value == '--' && userHrv != null) {  
331 - avgHrv.value = _formatMetric(userHrv);  
332 - }  
333 - if (userHrv != null) {  
334 - stressLabel.value = _hrvStatusTitle(  
335 - value: userHrv,  
336 - baseline: latestHrv.userHrvBaseline,  
337 - );  
338 - // 更新进度条压力分  
339 - stressScore.value = null;  
340 - //_computeStressScore(userHrv,baseline: latestHrv.userHrvBaseline,);  
341 - }  
342 } 266 }
343 267
344 void _applyHrvStatistics(HrvStatisticsData data, DateTime date) { 268 void _applyHrvStatistics(HrvStatisticsData data, DateTime date) {
@@ -365,36 +289,6 @@ class TodayController extends GetxController { @@ -365,36 +289,6 @@ class TodayController extends GetxController {
365 if (data.avgRestingHeartRate != null) { 289 if (data.avgRestingHeartRate != null) {
366 restingHeartRate.value = _formatMetric(data.avgRestingHeartRate); 290 restingHeartRate.value = _formatMetric(data.avgRestingHeartRate);
367 } 291 }
368 - if (data.avgSleepingHeartRate != null) {  
369 - sleepAverageHeartRate.value = _formatMetric(data.avgSleepingHeartRate);  
370 - }  
371 - }  
372 -  
373 - void _applySleepStatistics(SleepStatisticsData data, DateTime date) {  
374 - final dateKey = date.day;  
375 - final isToday = DateUtils.isSameDay(date, lastSelectableDay);  
376 -  
377 - if (isToday) {  
378 - if (sleepHours.value == '--' && data.avgSleepDuration != null) {  
379 - _applySleepDuration(data.avgSleepDuration);  
380 - }  
381 - } else {  
382 - int? dailySleep;  
383 - if (data.sleepTrendList != null) {  
384 - for (final trend in data.sleepTrendList!) {  
385 - if (trend.timeKey == dateKey) {  
386 - dailySleep = trend.average;  
387 - break;  
388 - }  
389 - }  
390 - }  
391 - _applySleepDuration(dailySleep);  
392 - }  
393 -  
394 - final deepPercentage = data.deepPercentage;  
395 - if (deepPercentage != null) {  
396 - sleepQuality.value = _sleepQualityFromDeepPercentage(deepPercentage);  
397 - }  
398 } 292 }
399 293
400 void _applyActivityStatistics( 294 void _applyActivityStatistics(
@@ -475,22 +369,6 @@ class TodayController extends GetxController { @@ -475,22 +369,6 @@ class TodayController extends GetxController {
475 if (value == null) return; 369 if (value == null) return;
476 } 370 }
477 371
478 - void _applySleepDuration(int? rawDuration) {  
479 - final minutes = _normalizeDurationMinutes(rawDuration);  
480 - if (minutes == null) {  
481 - sleepHours.value = '--';  
482 - sleepMinutes.value = '--';  
483 - sleepQuality.value = '--';  
484 - sleepProgress.value = 0;  
485 - return;  
486 - }  
487 -  
488 - sleepHours.value = '${minutes ~/ 60}';  
489 - sleepMinutes.value = '${minutes % 60}';  
490 - sleepQuality.value = _sleepQualityFromDuration(minutes);  
491 - sleepProgress.value = _progress(minutes.toDouble(), 8 * 60);  
492 - }  
493 -  
494 void _applyActivityRecentData(TodayStatusRecentData? recent) { 372 void _applyActivityRecentData(TodayStatusRecentData? recent) {
495 activityCalories.value = _formatMetric(recent?.move); 373 activityCalories.value = _formatMetric(recent?.move);
496 activityExerciseMinutes.value = _formatMetric(recent?.exercise); 374 activityExerciseMinutes.value = _formatMetric(recent?.exercise);
@@ -535,45 +413,6 @@ class TodayController extends GetxController { @@ -535,45 +413,6 @@ class TodayController extends GetxController {
535 return latest; 413 return latest;
536 } 414 }
537 415
538 - void _updateLatestHrvAnnotation(TodayHrvData? latest) {  
539 - if (latest?.time == null || latest?.value == null) {  
540 - hrvAnnotations.clear();  
541 - return;  
542 - }  
543 -  
544 - final hour = _hourFromApiTime(latest!.time!);  
545 - hrvAnnotations.assignAll([  
546 - HrvAnnotation(  
547 - hour: hour,  
548 - hrv: latest.value!,  
549 - stateLabel: latest.hrvStatus?.title ?? stressLabel.value,  
550 - detail: 'HRV ${_formatMetric(latest.value)}ms · ${_formatHour(hour)}',  
551 - ),  
552 - ]);  
553 - }  
554 -  
555 - /// 将 HRV 值映射为压力分 0~100(足基线时按相对偏差映射,无基线时按 100-HRV)。  
556 - int _computeStressScore(double hrv, {double? baseline}) {  
557 - if ((baseline ?? 0) > 0) {  
558 - final ratio = hrv / baseline!;  
559 - // ratio≥1.2 → 0~20(优秀)  
560 - if (ratio >= 1.2)  
561 - return ((1.2 - ratio.clamp(1.2, 1.5)) / 0.3 * 20).round().clamp(0, 20);  
562 - // ratio ∈ [0.9,1.2) → 21~50(正常)  
563 - if (ratio >= 0.9)  
564 - return (((1.2 - ratio) / 0.3) * 29 + 21).round().clamp(21, 50);  
565 - // ratio ∈ [0.8,0.9) → 51~80(注意)  
566 - if (ratio >= 0.8)  
567 - return (((0.9 - ratio) / 0.1) * 29 + 51).round().clamp(51, 80);  
568 - // ratio < 0.8 → 81~100(过载)  
569 - return ((0.8 - ratio.clamp(0.4, 0.8)) / 0.4 * 19 + 81)  
570 - .round()  
571 - .clamp(81, 100);  
572 - }  
573 - // 无基线:100 - HRV,HRV 越高 → 分数越低  
574 - return (100 - hrv).clamp(0, 100).round();  
575 - }  
576 -  
577 String _hrvStatusTitle({required double value, double? baseline}) { 416 String _hrvStatusTitle({required double value, double? baseline}) {
578 if ((baseline ?? 0) > 0) { 417 if ((baseline ?? 0) > 0) {
579 if (value >= 1.2 * baseline!) return HrvStatus.energetic.title; 418 if (value >= 1.2 * baseline!) return HrvStatus.energetic.title;
@@ -653,4 +492,49 @@ class TodayController extends GetxController { @@ -653,4 +492,49 @@ class TodayController extends GetxController {
653 final m = (totalMinutes % 60).toString().padLeft(2, '0'); 492 final m = (totalMinutes % 60).toString().padLeft(2, '0');
654 return '$h:$m'; 493 return '$h:$m';
655 } 494 }
  495 +
  496 + toTrendHrvPage() {
  497 + if (isFriend) {
  498 + Get.toNamed(
  499 + Routes.FRIEND_TREND,
  500 + arguments: FriendTrendArguments(
  501 + userId: 58,
  502 + name: 'xxa',
  503 + avatarUrl: 'asdfa',
  504 + ),
  505 + );
  506 + } else {
  507 + Get.find<HomeController>().openTrend(TrendType.hrv);
  508 + }
  509 + }
  510 +
  511 + toTrendSleepPage() {
  512 + if (isFriend) {
  513 + Get.toNamed(
  514 + Routes.FRIEND_TREND,
  515 + arguments: FriendTrendArguments(
  516 + userId: 58,
  517 + name: 'xxa',
  518 + avatarUrl: 'asdfa',
  519 + ),
  520 + );
  521 + } else {
  522 + Get.find<HomeController>().openTrend(TrendType.sleep);
  523 + }
  524 + }
  525 +
  526 + toTrendActivityPage() {
  527 + if (isFriend) {
  528 + Get.toNamed(
  529 + Routes.FRIEND_TREND,
  530 + arguments: FriendTrendArguments(
  531 + userId: 58,
  532 + name: 'xxa',
  533 + avatarUrl: 'asdfa',
  534 + ),
  535 + );
  536 + } else {
  537 + Get.find<HomeController>().openTrend(TrendType.activity);
  538 + }
  539 + }
656 } 540 }
  1 +import 'package:cached_network_image/cached_network_image.dart';
1 import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measurement_bottom_sheet.dart'; 2 import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measurement_bottom_sheet.dart';
2 import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; 3 import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart';
3 import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; 4 import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart';
@@ -81,14 +82,17 @@ class TodayTabBody extends StatelessWidget { @@ -81,14 +82,17 @@ class TodayTabBody extends StatelessWidget {
81 () => Column( 82 () => Column(
82 mainAxisSize: MainAxisSize.min, 83 mainAxisSize: MainAxisSize.min,
83 children: [ 84 children: [
84 - _TopDateBar(  
85 - title: _dateTitle(context, controller.selectedDate.value),  
86 - showBackToToday: !isSameDay(  
87 - controller.selectedDate.value,  
88 - controller.lastSelectableDay,  
89 - ),  
90 - onBackToToday: _backToToday,  
91 - ), 85 + controller.isFriend
  86 + ? _TopFriendBar()
  87 + : _TopDateBar(
  88 + title: _dateTitle(
  89 + context, controller.selectedDate.value),
  90 + showBackToToday: !isSameDay(
  91 + controller.selectedDate.value,
  92 + controller.lastSelectableDay,
  93 + ),
  94 + onBackToToday: _backToToday,
  95 + ),
92 _buildWeekCalendar(context), 96 _buildWeekCalendar(context),
93 ], 97 ],
94 ), 98 ),
@@ -156,7 +160,7 @@ class TodayTabBody extends StatelessWidget { @@ -156,7 +160,7 @@ class TodayTabBody extends StatelessWidget {
156 ), 160 ),
157 Obx( 161 Obx(
158 () => Text( 162 () => Text(
159 - controller.stressLabel.value, 163 + controller.v2StressScore.value?.stateString() ?? '',
160 textAlign: TextAlign.center, 164 textAlign: TextAlign.center,
161 style: TextStyle( 165 style: TextStyle(
162 color: context.colors.textPrimary, 166 color: context.colors.textPrimary,
@@ -168,7 +172,9 @@ class TodayTabBody extends StatelessWidget { @@ -168,7 +172,9 @@ class TodayTabBody extends StatelessWidget {
168 const SizedBox(height: 12), 172 const SizedBox(height: 12),
169 Obx( 173 Obx(
170 () => StressProgressBar( 174 () => StressProgressBar(
171 - score: controller.stressScore.value, 175 + score: controller.v2StressScore.value?.state == 0
  176 + ? null
  177 + : controller.v2StressScore.value?.comprehensiveScore,
172 ), 178 ),
173 ), 179 ),
174 ], 180 ],
@@ -429,6 +435,9 @@ class TodayTabBody extends StatelessWidget { @@ -429,6 +435,9 @@ class TodayTabBody extends StatelessWidget {
429 if (isSameDay(selectedDay, today.subtract(const Duration(days: 1)))) { 435 if (isSameDay(selectedDay, today.subtract(const Duration(days: 1)))) {
430 return context.l10n.yesterday; 436 return context.l10n.yesterday;
431 } 437 }
  438 + if (isSameDay(selectedDay, today.subtract(const Duration(days: 2)))) {
  439 + return '前天';
  440 + }
432 return MaterialLocalizations.of(context).formatShortMonthDay(selectedDay); 441 return MaterialLocalizations.of(context).formatShortMonthDay(selectedDay);
433 } 442 }
434 443
@@ -480,6 +489,8 @@ class _LatestHrvCard extends StatelessWidget { @@ -480,6 +489,8 @@ class _LatestHrvCard extends StatelessWidget {
480 final latestHrv = controller.v2LatestHrv.value?.latestHrv; 489 final latestHrv = controller.v2LatestHrv.value?.latestHrv;
481 final latestDataTime = controller.v2LatestHrv.value?.latestDataTime; 490 final latestDataTime = controller.v2LatestHrv.value?.latestDataTime;
482 final state = controller.v2LatestHrv.value?.state; 491 final state = controller.v2LatestHrv.value?.state;
  492 + final stateStr = controller.v2LatestHrv.value?.stateString();
  493 + final stateColor = controller.v2LatestHrv.value?.stateColor(context);
483 final tip = controller.v2LatestHrv.value?.tip; 494 final tip = controller.v2LatestHrv.value?.tip;
484 495
485 if (latestHrv == null || 496 if (latestHrv == null ||
@@ -488,11 +499,9 @@ class _LatestHrvCard extends StatelessWidget { @@ -488,11 +499,9 @@ class _LatestHrvCard extends StatelessWidget {
488 tip == null) { 499 tip == null) {
489 return const SizedBox.shrink(); 500 return const SizedBox.shrink();
490 } 501 }
491 - final latestHrvText = '$latestHrv ms';  
492 - final latestDataTimeText =  
493 - '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(latestDataTime * 1000))}';  
494 - final stateText =  
495 - '${state == 1 ? '状态优秀' : state == 2 ? '状态' : state == 3 ? '状态一般' : state == 4 ? '状态较差' : '状态极差'}'; 502 +
  503 + final latestDataTimeText = DateFormat('HH:mm')
  504 + .format(DateTime.fromMillisecondsSinceEpoch(latestDataTime * 1000));
496 return Container( 505 return Container(
497 padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), 506 padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
498 decoration: BoxDecoration( 507 decoration: BoxDecoration(
@@ -517,13 +526,13 @@ class _LatestHrvCard extends StatelessWidget { @@ -517,13 +526,13 @@ class _LatestHrvCard extends StatelessWidget {
517 width: 12, 526 width: 12,
518 height: 12, 527 height: 12,
519 decoration: BoxDecoration( 528 decoration: BoxDecoration(
520 - color: context.colors.chartGreen, 529 + color: stateColor,
521 shape: BoxShape.circle, 530 shape: BoxShape.circle,
522 ), 531 ),
523 ), 532 ),
524 const SizedBox(width: 8), 533 const SizedBox(width: 8),
525 Text( 534 Text(
526 - '${latestHrvText}ms·$stateText', 535 + '${latestHrv}ms · $stateStr',
527 style: TextStyle( 536 style: TextStyle(
528 color: colors.textPrimary, 537 color: colors.textPrimary,
529 fontSize: 18, 538 fontSize: 18,
@@ -550,6 +559,64 @@ class _LatestHrvCard extends StatelessWidget { @@ -550,6 +559,64 @@ class _LatestHrvCard extends StatelessWidget {
550 } 559 }
551 } 560 }
552 561
  562 +class _TopFriendBar extends StatelessWidget {
  563 + final _topBarHeight = 48.0;
  564 +
  565 + @override
  566 + Widget build(BuildContext context) {
  567 + return SizedBox(
  568 + height: _topBarHeight,
  569 + child: Padding(
  570 + padding: const EdgeInsets.symmetric(vertical: 4),
  571 + child: Row(
  572 + children: [
  573 + IconButton(
  574 + highlightColor: Colors.transparent,
  575 + padding: EdgeInsets.zero,
  576 + onPressed: Get.back,
  577 + icon: Image.asset(
  578 + 'assets/images/common/ic_nav_back.webp',
  579 + width: 24,
  580 + height: 24,
  581 + ),
  582 + ),
  583 + Container(
  584 + width: 28,
  585 + height: 28,
  586 + margin: EdgeInsets.only(left: 4, right: 8),
  587 + decoration: ShapeDecoration(
  588 + shape: RoundedRectangleBorder(
  589 + side: BorderSide(width: 0.78, color: Colors.white),
  590 + borderRadius: BorderRadius.circular(35.78),
  591 + ),
  592 + ),
  593 + child: CachedNetworkImage(imageUrl: 'imageUrl'),
  594 + ),
  595 + Text(
  596 + '男朋友的状态',
  597 + style: TextStyle(
  598 + color: Colors.black,
  599 + fontSize: 16,
  600 + fontWeight: FontWeight.w500,
  601 + ),
  602 + ),
  603 + Spacer(),
  604 + IconButton(
  605 + highlightColor: Colors.transparent,
  606 + padding: EdgeInsets.zero,
  607 + onPressed: Get.back,
  608 + icon: Image.asset(
  609 + 'assets/images/common/ic_replace.png',
  610 + width: 20,
  611 + height: 20,
  612 + ),
  613 + ),
  614 + ],
  615 + ),
  616 + ));
  617 + }
  618 +}
  619 +
553 class _TopDateBar extends StatelessWidget { 620 class _TopDateBar extends StatelessWidget {
554 const _TopDateBar({ 621 const _TopDateBar({
555 required this.title, 622 required this.title,
@@ -30,11 +30,10 @@ class AccountSettingView extends GetView<MyController> { @@ -30,11 +30,10 @@ class AccountSettingView extends GetView<MyController> {
30 highlightColor: Colors.transparent, 30 highlightColor: Colors.transparent,
31 padding: EdgeInsets.zero, 31 padding: EdgeInsets.zero,
32 onPressed: Get.back, 32 onPressed: Get.back,
33 - icon: Image( 33 + icon: Image.asset(
  34 + 'assets/images/common/ic_nav_back.webp',
34 width: 24, 35 width: 24,
35 height: 24, 36 height: 24,
36 - alignment: Alignment.centerLeft,  
37 - image: AssetImage(R.assetsImagesNavBackIcon),  
38 ), 37 ),
39 ), 38 ),
40 ), 39 ),
@@ -131,19 +131,25 @@ class TodayHrvChartCard extends StatelessWidget { @@ -131,19 +131,25 @@ class TodayHrvChartCard extends StatelessWidget {
131 ), 131 ),
132 ), 132 ),
133 const Spacer(), 133 const Spacer(),
134 - Text(  
135 - context.l10n.more,  
136 - style: TextStyle(  
137 - color: context.colors.textSecondary,  
138 - fontSize: 12,  
139 - fontWeight: FontWeight.w400, 134 + GestureDetector(
  135 + onTap: controller.toTrendHrvPage,
  136 + child: Text(
  137 + context.l10n.more,
  138 + style: TextStyle(
  139 + color: context.colors.textSecondary,
  140 + fontSize: 12,
  141 + fontWeight: FontWeight.w400,
  142 + ),
140 ), 143 ),
141 ), 144 ),
142 - Image.asset(  
143 - 'assets/images/common/ic_more_gray.png',  
144 - width: 16,  
145 - height: 16,  
146 - color: context.colors.textTertiary, 145 + GestureDetector(
  146 + onTap: controller.toTrendHrvPage,
  147 + child: Image.asset(
  148 + 'assets/images/common/ic_more_gray.png',
  149 + width: 16,
  150 + height: 16,
  151 + color: context.colors.textTertiary,
  152 + ),
147 ), 153 ),
148 ], 154 ],
149 ); 155 );
@@ -13,75 +13,83 @@ class TodaySleepCard extends StatelessWidget { @@ -13,75 +13,83 @@ class TodaySleepCard extends StatelessWidget {
13 13
14 @override 14 @override
15 Widget build(BuildContext context) { 15 Widget build(BuildContext context) {
16 - return Obx(  
17 - () => _TodaySummaryCard(  
18 - iconAsset: 'assets/images/common/ic_sleep_stroke.png',  
19 - iconColor: context.colors.primary,  
20 - title: context.l10n.sleep,  
21 - actionText: context.l10n.viewSleepReport,  
22 - progressValues: [controller.sleepProgress.value],  
23 - progressColors: [context.colors.primary],  
24 - metrics: [  
25 - _MetricData(  
26 - label: context.l10n.duration,  
27 - labelColor: context.colors.primary,  
28 - valueSpans: [  
29 - _MetricValueSpan(controller.sleepHours.value, '小时'),  
30 - _MetricValueSpan(controller.sleepMinutes.value, '分钟'),  
31 - ],  
32 - ),  
33 - _MetricData(  
34 - label: context.l10n.quality,  
35 - labelColor: const Color(0xFF7B9BFB),  
36 - valueSpans: [  
37 - _MetricValueSpan(  
38 - controller.sleepQuality.value,  
39 - '',  
40 - valueColor: _qualityColor(controller.sleepQuality.value),  
41 - ),  
42 - ],  
43 - ),  
44 - _MetricData(  
45 - label: context.l10n.averageHeartRate,  
46 - labelColor: context.colors.textSecondary,  
47 - valueSpans: [  
48 - _MetricValueSpan(controller.sleepAverageHeartRate.value, 'bpm'),  
49 - ],  
50 - ),  
51 - ],  
52 - rightWidget: Container(  
53 - width: 50,  
54 - height: 50,  
55 - margin: EdgeInsets.only(right: 6, left: 4),  
56 - child: CircularGradientCombineWidget(  
57 - size: 50,  
58 - duration: const Duration(seconds: 1),  
59 - sweepAngles: const [240, 210],  
60 - backgroundColors: const [  
61 - Color(0xFFF3F3F3),  
62 - Color(0xFFF3F3F3), 16 + return GestureDetector(
  17 + onTap: controller.toTrendSleepPage,
  18 + child: Obx(() {
  19 + var healthData = controller.v2HealthData.value;
  20 +
  21 + int sleepDuration = healthData?.sleepDuration ?? 0;
  22 + int sleepHours = sleepDuration ~/ 3600;
  23 + int sleepMinutes = sleepDuration % 60;
  24 + Color sleepStateColor =
  25 + healthData?.sleepStateColor(context) ?? context.colors.chartGreen;
  26 + String sleepStateStr = healthData?.sleepStateStr() ?? '--';
  27 + String sleepAverageHeartRate =
  28 + (healthData?.hrAvg == null || healthData?.hrAvg == 0)
  29 + ? '--'
  30 + : '${healthData?.hrAvg}';
  31 + double sleepDurationProgress =
  32 + ((healthData?.sleepDurationScore() ?? 0) * 3.60);
  33 + double sleepScoreProgress = (healthData?.sleepScore ?? 0.0) * 3.6;
  34 +
  35 + return _TodaySummaryCard(
  36 + iconAsset: 'assets/images/common/ic_sleep_stroke.png',
  37 + iconColor: context.colors.primary,
  38 + title: context.l10n.sleep,
  39 + actionText: context.l10n.viewSleepReport,
  40 + metrics: [
  41 + _MetricData(
  42 + label: context.l10n.duration,
  43 + labelColor: context.colors.primary,
  44 + valueSpans: [
  45 + _MetricValueSpan(sleepHours > 0 ? '$sleepHours' : '--', '小时'),
  46 + _MetricValueSpan(
  47 + sleepMinutes > 0 ? '$sleepMinutes' : '--', '分钟'),
63 ], 48 ],
64 - gradientColors: [  
65 - [context.colors.primary],  
66 - [context.colors.chartBlue], 49 + ),
  50 + _MetricData(
  51 + label: context.l10n.quality,
  52 + labelColor: const Color(0xFF7B9BFB),
  53 + valueSpans: [
  54 + _MetricValueSpan(
  55 + sleepStateStr,
  56 + '',
  57 + valueColor: sleepStateColor,
  58 + ),
67 ], 59 ],
68 - centerCircleSizeRatio: 0.3,  
69 - gapRatio: 0.1,  
70 - interpolatedColorRatio: 1),  
71 - ),  
72 - ), 60 + ),
  61 + _MetricData(
  62 + label: context.l10n.averageHeartRate,
  63 + labelColor: context.colors.textSecondary,
  64 + valueSpans: [
  65 + _MetricValueSpan(sleepAverageHeartRate, 'bpm'),
  66 + ],
  67 + ),
  68 + ],
  69 + rightWidget: Container(
  70 + width: 50,
  71 + height: 50,
  72 + margin: EdgeInsets.only(right: 6, left: 4),
  73 + child: CircularGradientCombineWidget(
  74 + size: 50,
  75 + duration: const Duration(seconds: 1),
  76 + sweepAngles: [sleepDurationProgress, sleepScoreProgress],
  77 + backgroundColors: const [
  78 + Color(0xFFF3F3F3),
  79 + Color(0xFFF3F3F3),
  80 + ],
  81 + gradientColors: [
  82 + [context.colors.primary],
  83 + [context.colors.chartBlue],
  84 + ],
  85 + centerCircleSizeRatio: 0.3,
  86 + gapRatio: 0.1,
  87 + interpolatedColorRatio: 1),
  88 + ),
  89 + );
  90 + }),
73 ); 91 );
74 } 92 }
75 -  
76 - Color _qualityColor(String quality) {  
77 - return switch (quality) {  
78 - '优秀' => const Color(0xFF3BD49D),  
79 - '良好' => const Color(0xFF7B9BFB),  
80 - '一般' => const Color(0xFFFF9A6E),  
81 - '偏少' => const Color(0xFFFF5279),  
82 - _ => const Color(0xFF0F0F11),  
83 - };  
84 - }  
85 } 93 }
86 94
87 class TodayActivityCard extends StatelessWidget { 95 class TodayActivityCard extends StatelessWidget {
@@ -91,91 +99,93 @@ class TodayActivityCard extends StatelessWidget { @@ -91,91 +99,93 @@ class TodayActivityCard extends StatelessWidget {
91 99
92 @override 100 @override
93 Widget build(BuildContext context) { 101 Widget build(BuildContext context) {
94 - return Obx(  
95 - () => _TodaySummaryCard(  
96 - iconAsset: 'assets/images/common/ic_exercise.png',  
97 - iconColor: const Color(0xFFFF5279),  
98 - title: context.l10n.fitness,  
99 - actionText: context.l10n.viewFitnessReport,  
100 - progressValues: [  
101 - controller.activityMoveProgress.value,  
102 - controller.activityExerciseProgress.value,  
103 - controller.activityStandProgress.value,  
104 - ],  
105 - progressColors: const [  
106 - Color(0xFFFF5279),  
107 - Color(0xFF3BD49D),  
108 - Color(0xFF7B9BFB),  
109 - ],  
110 - metrics: [  
111 - _MetricData(  
112 - label: context.l10n.event,  
113 - labelColor: const Color(0xFFFF5279),  
114 - valueSpans: [  
115 - _MetricValueSpan(controller.activityCalories.value, '千卡'),  
116 - ],  
117 - ),  
118 - _MetricData(  
119 - label: context.l10n.exercise,  
120 - labelColor: const Color(0xFF3BD49D),  
121 - valueSpans: [  
122 - _MetricValueSpan(  
123 - controller.activityExerciseMinutes.value,  
124 - '分钟',  
125 - ),  
126 - ],  
127 - ),  
128 - _MetricData(  
129 - label: context.l10n.standing,  
130 - labelColor: const Color(0xFF7B9BFB),  
131 - valueSpans: [  
132 - _MetricValueSpan(controller.activityStandHours.value, '小时'),  
133 - ],  
134 - ),  
135 - ],  
136 - rightWidget: Container(  
137 - width: 50,  
138 - height: 50,  
139 - margin: EdgeInsets.only(right: 6, left: 4),  
140 - child: Stack(  
141 - children: [  
142 - Container(  
143 - decoration: BoxDecoration(  
144 - shape: BoxShape.circle,  
145 - color: context.colors.textPrimary, 102 + return GestureDetector(
  103 + onTap: controller.toTrendActivityPage,
  104 + child: Obx(() {
  105 + var healthData = controller.v2HealthData.value;
  106 + var activity = healthData?.move;
  107 + var stand = healthData?.stand ?? 0;
  108 + var exercise = healthData?.exercise ?? 0;
  109 + var standStr = stand > 0 ? '${stand ~/ 3600}' : '--';
  110 + var exerciseStr = exercise > 0 ? '${exercise % 60}' : '--';
  111 +
  112 + return _TodaySummaryCard(
  113 + iconAsset: 'assets/images/common/ic_exercise.png',
  114 + iconColor: const Color(0xFFFF5279),
  115 + title: context.l10n.fitness,
  116 + actionText: context.l10n.viewFitnessReport,
  117 + metrics: [
  118 + _MetricData(
  119 + label: context.l10n.event,
  120 + labelColor: const Color(0xFFFF5279),
  121 + valueSpans: [
  122 + _MetricValueSpan(
  123 + (activity == null || activity == 0) ? '--' : '$activity',
  124 + '千卡'),
  125 + ],
  126 + ),
  127 + _MetricData(
  128 + label: context.l10n.exercise,
  129 + labelColor: const Color(0xFF3BD49D),
  130 + valueSpans: [
  131 + _MetricValueSpan(
  132 + exerciseStr,
  133 + '分钟',
146 ), 134 ),
147 - ),  
148 - CircularGradientCombineWidget(  
149 - size: 50,  
150 - duration: const Duration(seconds: 1),  
151 - centerCircleSizeRatio: 0.2,  
152 - gapRatio: 0.06,  
153 - sweepAngles: const [  
154 - 180,  
155 - 230,  
156 - 180,  
157 - ],  
158 - backgroundColors: [  
159 - context.colors.chartPink.withOpacity(0.4),  
160 - context.colors.chartGreen.withOpacity(0.3),  
161 - context.colors.chartBlue.withOpacity(0.4),  
162 - ],  
163 - gradientColors: [  
164 - [  
165 - context.colors.chartPink, 135 + ],
  136 + ),
  137 + _MetricData(
  138 + label: context.l10n.standing,
  139 + labelColor: const Color(0xFF7B9BFB),
  140 + valueSpans: [
  141 + _MetricValueSpan(standStr, '小时'),
  142 + ],
  143 + ),
  144 + ],
  145 + rightWidget: Container(
  146 + width: 50,
  147 + height: 50,
  148 + margin: EdgeInsets.only(right: 6, left: 4),
  149 + child: Stack(
  150 + children: [
  151 + Container(
  152 + decoration: BoxDecoration(
  153 + shape: BoxShape.circle,
  154 + color: context.colors.textPrimary,
  155 + ),
  156 + ),
  157 + CircularGradientCombineWidget(
  158 + size: 50,
  159 + duration: const Duration(seconds: 1),
  160 + centerCircleSizeRatio: 0.2,
  161 + gapRatio: 0.06,
  162 + sweepAngles: const [
  163 + 180,
  164 + 230,
  165 + 180,
166 ], 166 ],
167 - [  
168 - context.colors.chartGreen, 167 + backgroundColors: [
  168 + context.colors.chartPink.withOpacity(0.4),
  169 + context.colors.chartGreen.withOpacity(0.3),
  170 + context.colors.chartBlue.withOpacity(0.4),
169 ], 171 ],
170 - [  
171 - context.colors.chartBlue, 172 + gradientColors: [
  173 + [
  174 + context.colors.chartPink,
  175 + ],
  176 + [
  177 + context.colors.chartGreen,
  178 + ],
  179 + [
  180 + context.colors.chartBlue,
  181 + ],
172 ], 182 ],
173 - ],  
174 - ),  
175 - ], 183 + ),
  184 + ],
  185 + ),
176 ), 186 ),
177 - ),  
178 - ), 187 + );
  188 + }),
179 ); 189 );
180 } 190 }
181 } 191 }
@@ -187,8 +197,6 @@ class _TodaySummaryCard extends StatelessWidget { @@ -187,8 +197,6 @@ class _TodaySummaryCard extends StatelessWidget {
187 required this.title, 197 required this.title,
188 required this.actionText, 198 required this.actionText,
189 required this.metrics, 199 required this.metrics,
190 - required this.progressValues,  
191 - required this.progressColors,  
192 required this.rightWidget, 200 required this.rightWidget,
193 }); 201 });
194 202
@@ -197,8 +205,7 @@ class _TodaySummaryCard extends StatelessWidget { @@ -197,8 +205,7 @@ class _TodaySummaryCard extends StatelessWidget {
197 final String title; 205 final String title;
198 final String actionText; 206 final String actionText;
199 final List<_MetricData> metrics; 207 final List<_MetricData> metrics;
200 - final List<double> progressValues;  
201 - final List<Color> progressColors; 208 +
202 final Widget rightWidget; 209 final Widget rightWidget;
203 210
204 @override 211 @override
@@ -63,7 +63,9 @@ class LoginController extends GetxController { @@ -63,7 +63,9 @@ class LoginController extends GetxController {
63 } 63 }
64 final result = await PlatformHostApi().requestAppleSignIn(); 64 final result = await PlatformHostApi().requestAppleSignIn();
65 // result 为 null 或 identityToken 为空,说明用户取消或苹果授权失败 65 // result 为 null 或 identityToken 为空,说明用户取消或苹果授权失败
66 - if (result == null || result.identityToken?.isNotEmpty != true) { 66 + if (result == null ||
  67 + result.identityToken?.isNotEmpty != true ||
  68 + result.userId.isNotEmpty != true) {
67 appleSignInModel = null; 69 appleSignInModel = null;
68 return; 70 return;
69 } 71 }
@@ -184,26 +186,31 @@ class LoginController extends GetxController { @@ -184,26 +186,31 @@ class LoginController extends GetxController {
184 final loginRes = await _userApi.loginWithApple(appleModel); 186 final loginRes = await _userApi.loginWithApple(appleModel);
185 187
186 if (loginRes is AppSuccess<LoginResponse>) { 188 if (loginRes is AppSuccess<LoginResponse>) {
187 - await _handleLoginSuccess(loginRes.data, isApple: true); 189 + await _handleLoginSuccess(loginRes.data, appleModel: appleModel);
188 } else { 190 } else {
189 isLoggingIn.value = false; 191 isLoggingIn.value = false;
190 } 192 }
191 } 193 }
192 194
193 /// 登录/注册成功后的公共处理逻辑(手机号登录与苹果登录共用) 195 /// 登录/注册成功后的公共处理逻辑(手机号登录与苹果登录共用)
194 - Future<void> _handleLoginSuccess(  
195 - LoginResponse loginData, {  
196 - bool isApple = false,  
197 - }) async { 196 + Future<void> _handleLoginSuccess(LoginResponse loginData,
  197 + {AppleSignInModel? appleModel}) async {
  198 + bool isApple = appleModel != null;
198 final isRegister = loginData.isNewUser == true; 199 final isRegister = loginData.isNewUser == true;
199 String accessToken = loginData.accessToken; 200 String accessToken = loginData.accessToken;
200 201
201 - if (isRegister && !isApple) {  
202 - // 苹果登录时后端直接完成注册,无需二次注册请求  
203 - final regRes = await _userApi.register(  
204 - telephone: cleanPhone,  
205 - verifyCode: codeInput.value,  
206 - ); 202 + if (isRegister) {
  203 + final regRes = await _userApi.register(isApple
  204 + ? {
  205 + 'login_type': 'apple',
  206 + 'apple_user_id': appleModel.userId,
  207 + 'identity_token': appleModel.identityToken,
  208 + }
  209 + : {
  210 + 'login_type': 'verification_code',
  211 + 'telephone': cleanPhone,
  212 + 'verification_code': codeInput.value,
  213 + });
207 if (regRes is AppSuccess<RegisterResponse>) { 214 if (regRes is AppSuccess<RegisterResponse>) {
208 accessToken = regRes.data.accessToken; 215 accessToken = regRes.data.accessToken;
209 } else { 216 } else {
@@ -47,14 +47,18 @@ class UserApi { @@ -47,14 +47,18 @@ class UserApi {
47 Future<AppResult<LoginResponse>> loginWithApple(AppleSignInModel appleModel) { 47 Future<AppResult<LoginResponse>> loginWithApple(AppleSignInModel appleModel) {
48 return safeCall( 48 return safeCall(
49 call: () async { 49 call: () async {
  50 + var appleLoginRequest = AppleLoginRequest(
  51 + identityToken: appleModel.identityToken ?? '',
  52 + userId: appleModel.userId);
  53 + if (appleModel.nickname != null) {
  54 + appleLoginRequest.nickname = appleModel.nickname;
  55 + }
  56 + if (appleModel.email != null) {
  57 + appleLoginRequest.email = appleModel.email;
  58 + }
50 final response = await _dioClient.dio.post( 59 final response = await _dioClient.dio.post(
51 ApiPaths.userLogin, 60 ApiPaths.userLogin,
52 - data: AppleLoginRequest(  
53 - identityToken: appleModel.identityToken ?? '',  
54 - email: appleModel.email ?? '',  
55 - nickname: appleModel.nickname ?? '',  
56 - userId: appleModel.userId)  
57 - .toJson(), 61 + data: appleLoginRequest.toJson(),
58 options: _dioClient.noTokenOptions(), 62 options: _dioClient.noTokenOptions(),
59 ); 63 );
60 return LoginResponse.fromJson(response.data as Map<String, dynamic>); 64 return LoginResponse.fromJson(response.data as Map<String, dynamic>);
@@ -62,18 +66,12 @@ class UserApi { @@ -62,18 +66,12 @@ class UserApi {
62 ); 66 );
63 } 67 }
64 68
65 - Future<AppResult<RegisterResponse>> register({  
66 - required String telephone,  
67 - required String verifyCode,  
68 - }) { 69 + Future<AppResult<RegisterResponse>> register(Object? registerData) {
69 return safeCall( 70 return safeCall(
70 call: () async { 71 call: () async {
71 final response = await _dioClient.dio.post( 72 final response = await _dioClient.dio.post(
72 ApiPaths.userRegister, 73 ApiPaths.userRegister,
73 - data: RegisterRequest(  
74 - telephone: telephone,  
75 - verificationCode: verifyCode,  
76 - ).toJson(), 74 + data: registerData,
77 options: _dioClient.noTokenOptions(), 75 options: _dioClient.noTokenOptions(),
78 ); 76 );
79 return RegisterResponse.fromJson(response.data as Map<String, dynamic>); 77 return RegisterResponse.fromJson(response.data as Map<String, dynamic>);
  1 +import 'package:doublefeel_flutter/core/theme/app_theme.dart';
  2 +import 'package:flutter/src/widgets/framework.dart';
  3 +
1 int? _parseInt(dynamic value) { 4 int? _parseInt(dynamic value) {
2 if (value == null) return null; 5 if (value == null) return null;
3 if (value is int) return value; 6 if (value is int) return value;
@@ -28,6 +31,34 @@ class V2LatestHrvData { @@ -28,6 +31,34 @@ class V2LatestHrvData {
28 final int? state; 31 final int? state;
29 final String? tip; 32 final String? tip;
30 33
  34 + String stateString() {
  35 + switch (state) {
  36 + case 1:
  37 + return '压力过载';
  38 + case 2:
  39 + return '注意压力';
  40 + case 3:
  41 + return '状态正常';
  42 + case 4:
  43 + return '状态优秀';
  44 + }
  45 + return '等待数据';
  46 + }
  47 +
  48 + stateColor(BuildContext context) {
  49 + switch (state) {
  50 + case 1:
  51 + return context.colors.chartPink;
  52 + case 2:
  53 + return context.colors.chartOrange;
  54 + case 3:
  55 + return context.colors.chartGreen;
  56 + case 4:
  57 + return context.colors.chartBlue;
  58 + }
  59 + return context.colors.chartGreen;
  60 + }
  61 +
31 factory V2LatestHrvData.fromJson(Map<String, dynamic> json) { 62 factory V2LatestHrvData.fromJson(Map<String, dynamic> json) {
32 return V2LatestHrvData( 63 return V2LatestHrvData(
33 latestHrv: _parseDouble(json['latest_hrv']), 64 latestHrv: _parseDouble(json['latest_hrv']),
@@ -95,6 +126,43 @@ class V2HealthData { @@ -95,6 +126,43 @@ class V2HealthData {
95 if (sleepState != null) val['sleep_state'] = sleepState; 126 if (sleepState != null) val['sleep_state'] = sleepState;
96 return val; 127 return val;
97 } 128 }
  129 +
  130 + /// 根据睡眠时长(秒)计算得分。
  131 + int sleepDurationScore() {
  132 + final duration = sleepDuration;
  133 + if (duration == null || duration <= 0) return 0;
  134 + final hours = duration / 3600.0;
  135 + if (hours >= 7 && hours <= 9) return 100;
  136 + if (hours > 9 && hours <= 10) return 80;
  137 + if (hours >= 6 && hours < 7) return 80;
  138 + if (hours >= 5 && hours < 6) return 60;
  139 + if (hours > 10) return 50;
  140 + return 30; // < 5h
  141 + }
  142 +
  143 + sleepStateColor(BuildContext context) {
  144 + switch (sleepState) {
  145 + case 1:
  146 + return context.colors.chartBlue;
  147 + case 2:
  148 + return context.colors.chartGreen;
  149 + case 3:
  150 + return context.colors.chartPink;
  151 + }
  152 + return context.colors.chartGreen;
  153 + }
  154 +
  155 + sleepStateStr() {
  156 + switch (sleepState) {
  157 + case 1:
  158 + return '睡得很棒';
  159 + case 2:
  160 + return '睡得不错';
  161 + case 3:
  162 + return '睡得差';
  163 + }
  164 + return '--';
  165 + }
98 } 166 }
99 167
100 /// v2/stress_score/ response 168 /// v2/stress_score/ response
@@ -134,6 +202,20 @@ class V2StressScore { @@ -134,6 +202,20 @@ class V2StressScore {
134 } 202 }
135 return val; 203 return val;
136 } 204 }
  205 +
  206 + String stateString() {
  207 + switch (state) {
  208 + case 1:
  209 + return '压力过载';
  210 + case 2:
  211 + return '注意压力';
  212 + case 3:
  213 + return '状态正常';
  214 + case 4:
  215 + return '状态优秀';
  216 + }
  217 + return '等待数据';
  218 + }
137 } 219 }
138 220
139 /// Single item in v2/hrv_trend/ list 221 /// Single item in v2/hrv_trend/ list
@@ -19,7 +19,7 @@ class LoginRequest { @@ -19,7 +19,7 @@ class LoginRequest {
19 } 19 }
20 20
21 class AppleLoginRequest { 21 class AppleLoginRequest {
22 - const AppleLoginRequest({ 22 + AppleLoginRequest({
23 this.loginType = 'apple', 23 this.loginType = 'apple',
24 required this.identityToken, 24 required this.identityToken,
25 required this.userId, 25 required this.userId,
@@ -30,8 +30,8 @@ class AppleLoginRequest { @@ -30,8 +30,8 @@ class AppleLoginRequest {
30 final String loginType; 30 final String loginType;
31 final String identityToken; 31 final String identityToken;
32 final String userId; 32 final String userId;
33 - final String? email;  
34 - final String? nickname; 33 + String? email;
  34 + String? nickname;
35 35
36 Map<String, dynamic> toJson() => { 36 Map<String, dynamic> toJson() => {
37 'login_type': loginType, 37 'login_type': loginType,
@@ -42,21 +42,6 @@ class AppleLoginRequest { @@ -42,21 +42,6 @@ class AppleLoginRequest {
42 }; 42 };
43 } 43 }
44 44
45 -class RegisterRequest {  
46 - const RegisterRequest({  
47 - required this.telephone,  
48 - required this.verificationCode,  
49 - });  
50 -  
51 - final String telephone;  
52 - final String verificationCode;  
53 -  
54 - Map<String, dynamic> toJson() => {  
55 - 'telephone': telephone,  
56 - 'verification_code': verificationCode,  
57 - };  
58 -}  
59 -  
60 class VerifyCodeRequest { 45 class VerifyCodeRequest {
61 const VerifyCodeRequest({required this.telephone, required this.scene}); 46 const VerifyCodeRequest({required this.telephone, required this.scene});
62 47
@@ -193,19 +178,20 @@ class RegisterResponse { @@ -193,19 +178,20 @@ class RegisterResponse {
193 } 178 }
194 179
195 class UserInfoResponse { 180 class UserInfoResponse {
196 - const UserInfoResponse({  
197 - this.id,  
198 - this.pairCode,  
199 - this.pairId,  
200 - this.telephone,  
201 - this.nickname,  
202 - this.avatar,  
203 - this.persona,  
204 - this.status,  
205 - this.lastLoginTime,  
206 - this.createTime,  
207 - this.isBot,  
208 - }); 181 + const UserInfoResponse(
  182 + {this.id,
  183 + this.pairCode,
  184 + this.pairId,
  185 + this.telephone,
  186 + this.nickname,
  187 + this.avatar,
  188 + this.persona,
  189 + this.status,
  190 + this.lastLoginTime,
  191 + this.createTime,
  192 + this.isBot,
  193 + this.uniqueCode,
  194 + this.enableAddWithUCode});
209 195
210 final int? id; 196 final int? id;
211 final String? pairCode; 197 final String? pairCode;
@@ -218,6 +204,8 @@ class UserInfoResponse { @@ -218,6 +204,8 @@ class UserInfoResponse {
218 final int? lastLoginTime; 204 final int? lastLoginTime;
219 final int? createTime; 205 final int? createTime;
220 final int? isBot; 206 final int? isBot;
  207 + final String? uniqueCode;
  208 + final int? enableAddWithUCode;
221 209
222 factory UserInfoResponse.fromJson(Map<String, dynamic> json) { 210 factory UserInfoResponse.fromJson(Map<String, dynamic> json) {
223 return UserInfoResponse( 211 return UserInfoResponse(
@@ -232,6 +220,8 @@ class UserInfoResponse { @@ -232,6 +220,8 @@ class UserInfoResponse {
232 lastLoginTime: json['last_login_time'] as int?, 220 lastLoginTime: json['last_login_time'] as int?,
233 createTime: json['create_time'] as int?, 221 createTime: json['create_time'] as int?,
234 isBot: json['is_bot'] as int?, 222 isBot: json['is_bot'] as int?,
  223 + uniqueCode: json['unique_code'] as String?,
  224 + enableAddWithUCode: json['enable_add_with_ucode'] as int?,
235 ); 225 );
236 } 226 }
237 227
@@ -248,6 +238,10 @@ class UserInfoResponse { @@ -248,6 +238,10 @@ class UserInfoResponse {
248 if (lastLoginTime != null) val['last_login_time'] = lastLoginTime; 238 if (lastLoginTime != null) val['last_login_time'] = lastLoginTime;
249 if (createTime != null) val['create_time'] = createTime; 239 if (createTime != null) val['create_time'] = createTime;
250 if (isBot != null) val['is_bot'] = isBot; 240 if (isBot != null) val['is_bot'] = isBot;
  241 + if (uniqueCode != null) val['unique_code'] = uniqueCode;
  242 + if (enableAddWithUCode != null) {
  243 + val['enable_add_with_ucode'] = enableAddWithUCode;
  244 + }
251 return val; 245 return val;
252 } 246 }
253 } 247 }