Commit aa24744a608708019bffd7a87cab72fd235863ba

Authored by 常守达
1 parent e1d8b4de

feat(today): 无心率数据页

Showing 30 changed files with 1231 additions and 728 deletions
1 -<?xml version="1.0" encoding="UTF-8"?>  
2 -<Scheme  
3 - LastUpgradeVersion = "2600"  
4 - version = "2.0">  
5 - <BuildAction  
6 - parallelizeBuildables = "YES"  
7 - buildImplicitDependencies = "YES"  
8 - buildArchitectures = "Automatic">  
9 - <BuildActionEntries>  
10 - <BuildActionEntry  
11 - buildForTesting = "YES"  
12 - buildForRunning = "YES"  
13 - buildForProfiling = "YES"  
14 - buildForArchiving = "YES"  
15 - buildForAnalyzing = "YES">  
16 - <BuildableReference  
17 - BuildableIdentifier = "primary"  
18 - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4"  
19 - BuildableName = "Runner Watch App.app"  
20 - BlueprintName = "Runner Watch App"  
21 - ReferencedContainer = "container:Runner.xcodeproj">  
22 - </BuildableReference>  
23 - </BuildActionEntry>  
24 - <BuildActionEntry  
25 - buildForTesting = "YES"  
26 - buildForRunning = "YES"  
27 - buildForProfiling = "YES"  
28 - buildForArchiving = "YES"  
29 - buildForAnalyzing = "YES">  
30 - <BuildableReference  
31 - BuildableIdentifier = "primary"  
32 - BlueprintIdentifier = "66FBE57D2FDA4F0E00F515B4"  
33 - BuildableName = "Runner.app"  
34 - BlueprintName = "Runner"  
35 - ReferencedContainer = "container:Runner.xcodeproj">  
36 - </BuildableReference>  
37 - </BuildActionEntry>  
38 - </BuildActionEntries>  
39 - </BuildAction>  
40 - <TestAction  
41 - buildConfiguration = "Debug"  
42 - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"  
43 - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"  
44 - shouldUseLaunchSchemeArgsEnv = "YES"  
45 - shouldAutocreateTestPlan = "YES">  
46 - </TestAction>  
47 - <LaunchAction  
48 - buildConfiguration = "Debug"  
49 - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"  
50 - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"  
51 - launchStyle = "0"  
52 - useCustomWorkingDirectory = "NO"  
53 - ignoresPersistentStateOnLaunch = "NO"  
54 - debugDocumentVersioning = "YES"  
55 - debugServiceExtension = "internal"  
56 - allowLocationSimulation = "YES"  
57 - launchAutomaticallySubstyle = "8">  
58 - <BuildableProductRunnable  
59 - runnableDebuggingMode = "0">  
60 - <BuildableReference  
61 - BuildableIdentifier = "primary"  
62 - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4"  
63 - BuildableName = "Runner Watch App.app"  
64 - BlueprintName = "Runner Watch App"  
65 - ReferencedContainer = "container:Runner.xcodeproj">  
66 - </BuildableReference>  
67 - </BuildableProductRunnable>  
68 - </LaunchAction>  
69 - <ProfileAction  
70 - buildConfiguration = "Release"  
71 - shouldUseLaunchSchemeArgsEnv = "YES"  
72 - savedToolIdentifier = ""  
73 - useCustomWorkingDirectory = "NO"  
74 - debugDocumentVersioning = "YES"  
75 - launchAutomaticallySubstyle = "8">  
76 - <BuildableProductRunnable  
77 - runnableDebuggingMode = "0">  
78 - <BuildableReference  
79 - BuildableIdentifier = "primary"  
80 - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4"  
81 - BuildableName = "Runner Watch App.app"  
82 - BlueprintName = "Runner Watch App"  
83 - ReferencedContainer = "container:Runner.xcodeproj">  
84 - </BuildableReference>  
85 - </BuildableProductRunnable>  
86 - </ProfileAction>  
87 - <AnalyzeAction  
88 - buildConfiguration = "Debug">  
89 - </AnalyzeAction>  
90 - <ArchiveAction  
91 - buildConfiguration = "Release"  
92 - revealArchiveInOrganizer = "YES">  
93 - </ArchiveAction>  
94 -</Scheme>  
1 -<?xml version="1.0" encoding="UTF-8"?>  
2 -<Scheme  
3 - LastUpgradeVersion = "2600"  
4 - version = "1.7">  
5 - <BuildAction  
6 - parallelizeBuildables = "YES"  
7 - buildImplicitDependencies = "YES"  
8 - buildArchitectures = "Automatic">  
9 - <BuildActionEntries>  
10 - <BuildActionEntry  
11 - buildForTesting = "YES"  
12 - buildForRunning = "YES"  
13 - buildForProfiling = "YES"  
14 - buildForArchiving = "YES"  
15 - buildForAnalyzing = "YES">  
16 - <BuildableReference  
17 - BuildableIdentifier = "primary"  
18 - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4"  
19 - BuildableName = "Runner Watch App.app"  
20 - BlueprintName = "Runner Watch App"  
21 - ReferencedContainer = "container:Runner.xcodeproj">  
22 - </BuildableReference>  
23 - </BuildActionEntry>  
24 - <BuildActionEntry  
25 - buildForTesting = "YES"  
26 - buildForRunning = "YES"  
27 - buildForProfiling = "YES"  
28 - buildForArchiving = "YES"  
29 - buildForAnalyzing = "YES">  
30 - <BuildableReference  
31 - BuildableIdentifier = "primary"  
32 - BlueprintIdentifier = "66FBE57D2FDA4F0E00F515B4"  
33 - BuildableName = "Runner.app"  
34 - BlueprintName = "Runner"  
35 - ReferencedContainer = "container:Runner.xcodeproj">  
36 - </BuildableReference>  
37 - </BuildActionEntry>  
38 - </BuildActionEntries>  
39 - </BuildAction>  
40 - <TestAction  
41 - buildConfiguration = "Debug"  
42 - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"  
43 - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"  
44 - shouldUseLaunchSchemeArgsEnv = "YES"  
45 - shouldAutocreateTestPlan = "YES">  
46 - </TestAction>  
47 - <LaunchAction  
48 - buildConfiguration = "Debug"  
49 - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"  
50 - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"  
51 - launchStyle = "0"  
52 - useCustomWorkingDirectory = "NO"  
53 - ignoresPersistentStateOnLaunch = "NO"  
54 - debugDocumentVersioning = "YES"  
55 - debugServiceExtension = "internal"  
56 - allowLocationSimulation = "YES">  
57 - <BuildableProductRunnable  
58 - runnableDebuggingMode = "0">  
59 - <BuildableReference  
60 - BuildableIdentifier = "primary"  
61 - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4"  
62 - BuildableName = "Runner Watch App.app"  
63 - BlueprintName = "Runner Watch App"  
64 - ReferencedContainer = "container:Runner.xcodeproj">  
65 - </BuildableReference>  
66 - </BuildableProductRunnable>  
67 - </LaunchAction>  
68 - <ProfileAction  
69 - buildConfiguration = "Release"  
70 - shouldUseLaunchSchemeArgsEnv = "YES"  
71 - savedToolIdentifier = ""  
72 - useCustomWorkingDirectory = "NO"  
73 - debugDocumentVersioning = "YES">  
74 - <BuildableProductRunnable  
75 - runnableDebuggingMode = "0">  
76 - <BuildableReference  
77 - BuildableIdentifier = "primary"  
78 - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4"  
79 - BuildableName = "Runner Watch App.app"  
80 - BlueprintName = "Runner Watch App"  
81 - ReferencedContainer = "container:Runner.xcodeproj">  
82 - </BuildableReference>  
83 - </BuildableProductRunnable>  
84 - </ProfileAction>  
85 - <AnalyzeAction  
86 - buildConfiguration = "Debug">  
87 - </AnalyzeAction>  
88 - <ArchiveAction  
89 - buildConfiguration = "Release"  
90 - revealArchiveInOrganizer = "YES">  
91 - </ArchiveAction>  
92 -</Scheme>  
1 -<?xml version="1.0" encoding="UTF-8"?>  
2 -<Scheme  
3 - LastUpgradeVersion = "2600"  
4 - version = "2.0">  
5 - <BuildAction  
6 - parallelizeBuildables = "YES"  
7 - buildImplicitDependencies = "YES"  
8 - buildArchitectures = "Automatic">  
9 - <BuildActionEntries>  
10 - <BuildActionEntry  
11 - buildForTesting = "YES"  
12 - buildForRunning = "YES"  
13 - buildForProfiling = "YES"  
14 - buildForArchiving = "YES"  
15 - buildForAnalyzing = "YES">  
16 - <BuildableReference  
17 - BuildableIdentifier = "primary"  
18 - BlueprintIdentifier = "66FBE5A42FDA518C00F515B4"  
19 - BuildableName = "doublefeel-watch-app-extensionExtension.appex"  
20 - BlueprintName = "doublefeel-watch-app-extensionExtension"  
21 - ReferencedContainer = "container:Runner.xcodeproj">  
22 - </BuildableReference>  
23 - </BuildActionEntry>  
24 - <BuildActionEntry  
25 - buildForTesting = "YES"  
26 - buildForRunning = "YES"  
27 - buildForProfiling = "YES"  
28 - buildForArchiving = "YES"  
29 - buildForAnalyzing = "YES">  
30 - <BuildableReference  
31 - BuildableIdentifier = "primary"  
32 - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4"  
33 - BuildableName = "Runner Watch App.app"  
34 - BlueprintName = "Runner Watch App"  
35 - ReferencedContainer = "container:Runner.xcodeproj">  
36 - </BuildableReference>  
37 - </BuildActionEntry>  
38 - <BuildActionEntry  
39 - buildForTesting = "YES"  
40 - buildForRunning = "YES"  
41 - buildForProfiling = "YES"  
42 - buildForArchiving = "YES"  
43 - buildForAnalyzing = "YES">  
44 - <BuildableReference  
45 - BuildableIdentifier = "primary"  
46 - BlueprintIdentifier = "66FBE57D2FDA4F0E00F515B4"  
47 - BuildableName = "Runner.app"  
48 - BlueprintName = "Runner"  
49 - ReferencedContainer = "container:Runner.xcodeproj">  
50 - </BuildableReference>  
51 - </BuildActionEntry>  
52 - </BuildActionEntries>  
53 - </BuildAction>  
54 - <TestAction  
55 - buildConfiguration = "Debug"  
56 - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"  
57 - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"  
58 - shouldUseLaunchSchemeArgsEnv = "YES"  
59 - shouldAutocreateTestPlan = "YES">  
60 - </TestAction>  
61 - <LaunchAction  
62 - buildConfiguration = "Debug"  
63 - selectedDebuggerIdentifier = ""  
64 - selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"  
65 - launchStyle = "0"  
66 - askForAppToLaunch = "Yes"  
67 - useCustomWorkingDirectory = "NO"  
68 - ignoresPersistentStateOnLaunch = "NO"  
69 - debugDocumentVersioning = "YES"  
70 - debugServiceExtension = "internal"  
71 - allowLocationSimulation = "YES"  
72 - launchAutomaticallySubstyle = "2">  
73 - <BuildableProductRunnable  
74 - runnableDebuggingMode = "0">  
75 - <BuildableReference  
76 - BuildableIdentifier = "primary"  
77 - BlueprintIdentifier = "66FBE57D2FDA4F0E00F515B4"  
78 - BuildableName = "Runner.app"  
79 - BlueprintName = "Runner"  
80 - ReferencedContainer = "container:Runner.xcodeproj">  
81 - </BuildableReference>  
82 - </BuildableProductRunnable>  
83 - <EnvironmentVariables>  
84 - <EnvironmentVariable  
85 - key = "_XCWidgetKind"  
86 - value = ""  
87 - isEnabled = "YES">  
88 - </EnvironmentVariable>  
89 - <EnvironmentVariable  
90 - key = "_XCWidgetDefaultView"  
91 - value = "timeline"  
92 - isEnabled = "YES">  
93 - </EnvironmentVariable>  
94 - <EnvironmentVariable  
95 - key = "_XCWidgetFamily"  
96 - value = "systemMedium"  
97 - isEnabled = "YES">  
98 - </EnvironmentVariable>  
99 - </EnvironmentVariables>  
100 - </LaunchAction>  
101 - <ProfileAction  
102 - buildConfiguration = "Release"  
103 - shouldUseLaunchSchemeArgsEnv = "YES"  
104 - savedToolIdentifier = ""  
105 - useCustomWorkingDirectory = "NO"  
106 - debugDocumentVersioning = "YES">  
107 - <BuildableProductRunnable  
108 - runnableDebuggingMode = "0">  
109 - <BuildableReference  
110 - BuildableIdentifier = "primary"  
111 - BlueprintIdentifier = "66FBE5A42FDA518C00F515B4"  
112 - BuildableName = "doublefeel-watch-app-extensionExtension.appex"  
113 - BlueprintName = "doublefeel-watch-app-extensionExtension"  
114 - ReferencedContainer = "container:Runner.xcodeproj">  
115 - </BuildableReference>  
116 - </BuildableProductRunnable>  
117 - </ProfileAction>  
118 - <AnalyzeAction  
119 - buildConfiguration = "Debug">  
120 - </AnalyzeAction>  
121 - <ArchiveAction  
122 - buildConfiguration = "Release"  
123 - revealArchiveInOrganizer = "YES">  
124 - </ArchiveAction>  
125 -</Scheme>  
1 import 'package:doublefeel_flutter/app/widget/dash_divider.dart'; 1 import 'package:doublefeel_flutter/app/widget/dash_divider.dart';
  2 +import 'package:doublefeel_flutter/core/theme/app_theme.dart';
2 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
3 import 'package:flutter/services.dart'; 4 import 'package:flutter/services.dart';
4 5
@@ -95,7 +96,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -95,7 +96,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
95 style: TextStyle( 96 style: TextStyle(
96 fontSize: 20, 97 fontSize: 20,
97 fontWeight: FontWeight.w600, 98 fontWeight: FontWeight.w600,
98 - color: const Color(0xFF0F0F11), 99 + color: context.colors.textPrimary,
99 height: 1.4, 100 height: 1.4,
100 ), 101 ),
101 ), 102 ),
@@ -110,7 +111,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -110,7 +111,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
110 borderRadius: BorderRadius.circular(20), 111 borderRadius: BorderRadius.circular(20),
111 boxShadow: [ 112 boxShadow: [
112 BoxShadow( 113 BoxShadow(
113 - color: const Color(0xFF845EEE).withValues(alpha: 0.08), 114 + color: context.colors.primary.withValues(alpha: 0.08),
114 blurRadius: 24, 115 blurRadius: 24,
115 offset: const Offset(0, 8), 116 offset: const Offset(0, 8),
116 ), 117 ),
@@ -134,7 +135,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -134,7 +135,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
134 onTap: controller.onSubmitPartnerId, 135 onTap: controller.onSubmitPartnerId,
135 enabled: controller.canSubmit && !controller.isSubmitting.value, 136 enabled: controller.canSubmit && !controller.isSubmitting.value,
136 isLoading: controller.isSubmitting.value, 137 isLoading: controller.isSubmitting.value,
137 - primaryColor: const Color(0xFFFF9A6E), 138 + primaryColor: context.colors.chartOrange,
138 ); 139 );
139 }), 140 }),
140 ), 141 ),
@@ -156,7 +157,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -156,7 +157,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
156 style: TextStyle( 157 style: TextStyle(
157 fontSize: 14, 158 fontSize: 14,
158 fontWeight: FontWeight.w400, 159 fontWeight: FontWeight.w400,
159 - color: const Color(0xFF0F0F11), 160 + color: context.colors.textPrimary,
160 ), 161 ),
161 ), 162 ),
162 SizedBox(height: 13), 163 SizedBox(height: 13),
@@ -170,7 +171,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -170,7 +171,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
170 style: TextStyle( 171 style: TextStyle(
171 fontSize: 32, 172 fontSize: 32,
172 fontWeight: FontWeight.w600, 173 fontWeight: FontWeight.w600,
173 - color: const Color(0xFF845EEE), 174 + color: context.colors.primary,
174 height: 1.4, 175 height: 1.4,
175 ), 176 ),
176 ), 177 ),
@@ -189,10 +190,9 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -189,10 +190,9 @@ class BindPartnerView extends GetView<BindPartnerController> {
189 color: const Color(0xFFF0F0F6), 190 color: const Color(0xFFF0F0F6),
190 borderRadius: BorderRadius.circular(6), 191 borderRadius: BorderRadius.circular(6),
191 ), 192 ),
192 - child: Icon(  
193 - Icons.copy_rounded,  
194 - size: 12,  
195 - color: const Color(0xFF845EEE), 193 + padding: EdgeInsets.all(3),
  194 + child: Image.asset(
  195 + 'assets/images/common/ic_copy_purple.webp',
196 ), 196 ),
197 ), 197 ),
198 ), 198 ),
@@ -205,6 +205,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -205,6 +205,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
205 label: context.l10n.bindPartnerShareMyCode, 205 label: context.l10n.bindPartnerShareMyCode,
206 onTap: controller.onShareMyCode, 206 onTap: controller.onShareMyCode,
207 enabled: true, 207 enabled: true,
  208 + primaryColor: context.colors.primary,
208 ), 209 ),
209 ], 210 ],
210 ), 211 ),
@@ -233,7 +234,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -233,7 +234,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
233 style: TextStyle( 234 style: TextStyle(
234 fontSize: 14, 235 fontSize: 14,
235 fontWeight: FontWeight.w400, 236 fontWeight: FontWeight.w400,
236 - color: const Color(0xFF845EEE), 237 + color: context.colors.primary,
237 ), 238 ),
238 ), 239 ),
239 ), 240 ),
@@ -260,7 +261,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -260,7 +261,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
260 style: TextStyle( 261 style: TextStyle(
261 fontSize: 14, 262 fontSize: 14,
262 fontWeight: FontWeight.w400, 263 fontWeight: FontWeight.w400,
263 - color: const Color(0xFF0F0F11), 264 + color: context.colors.textPrimary,
264 ), 265 ),
265 ), 266 ),
266 SizedBox(height: 16), 267 SizedBox(height: 16),
@@ -278,7 +279,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -278,7 +279,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
278 style: TextStyle( 279 style: TextStyle(
279 fontSize: 32, 280 fontSize: 32,
280 fontWeight: FontWeight.w400, 281 fontWeight: FontWeight.w400,
281 - color: const Color(0xFF0F0F11), 282 + color: context.colors.textPrimary,
282 ), 283 ),
283 decoration: InputDecoration( 284 decoration: InputDecoration(
284 hintText: context.l10n.bindPartnerInputHint, 285 hintText: context.l10n.bindPartnerInputHint,
@@ -303,7 +304,7 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -303,7 +304,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
303 required VoidCallback onTap, 304 required VoidCallback onTap,
304 bool enabled = true, 305 bool enabled = true,
305 bool isLoading = false, 306 bool isLoading = false,
306 - Color primaryColor = const Color(0xFF845EEE), 307 + required Color primaryColor,
307 }) { 308 }) {
308 return GestureDetector( 309 return GestureDetector(
309 onTap: enabled ? onTap : null, 310 onTap: enabled ? onTap : null,
1 import 'dart:async'; 1 import 'dart:async';
2 2
3 import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; 3 import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart';
  4 +import 'package:doublefeel_flutter/app/routes/app_pages.dart';
4 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 5 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
5 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 6 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
6 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 7 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
@@ -11,7 +12,6 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart'; @@ -11,7 +12,6 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart';
11 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; 12 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
12 import 'package:doublefeel_flutter/data/models/health/health_models.dart'; 13 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_upload_models.dart';
14 -import 'package:doublefeel_flutter/data/models/user/user_models.dart';  
15 import 'package:flutter/material.dart'; 15 import 'package:flutter/material.dart';
16 import 'package:get/get.dart'; 16 import 'package:get/get.dart';
17 17
@@ -66,6 +66,9 @@ class TodayController extends GetxController { @@ -66,6 +66,9 @@ class TodayController extends GetxController {
66 // ── 压力状态 ───────────────────────────── 66 // ── 压力状态 ─────────────────────────────
67 final stressLabel = '状态正常'.obs; 67 final stressLabel = '状态正常'.obs;
68 68
  69 + /// 进度条压力分(null=加载中, 0~100=压力分: 0优秀, 100过载)
  70 + final stressScore = Rxn<int>();
  71 +
69 // ── HRV 表盘引导 Banner ─────────────────── 72 // ── HRV 表盘引导 Banner ───────────────────
70 final showHrvAdBanner = true.obs; 73 final showHrvAdBanner = true.obs;
71 final showPartnerAdBanner = true.obs; 74 final showPartnerAdBanner = true.obs;
@@ -77,8 +80,6 @@ class TodayController extends GetxController { @@ -77,8 +80,6 @@ class TodayController extends GetxController {
77 // ── HRV 数字 + 心率 ─────────────────────── 80 // ── HRV 数字 + 心率 ───────────────────────
78 final avgHrv = '--'.obs; 81 final avgHrv = '--'.obs;
79 final restingHeartRate = '--'.obs; 82 final restingHeartRate = '--'.obs;
80 - final latestHrvValue = '--'.obs;  
81 - final latestHrvTimeLabel = ''.obs;  
82 83
83 // ── 睡眠卡片 ────────────────────────────── 84 // ── 睡眠卡片 ──────────────────────────────
84 final sleepHours = '--'.obs; 85 final sleepHours = '--'.obs;
@@ -110,7 +111,12 @@ class TodayController extends GetxController { @@ -110,7 +111,12 @@ class TodayController extends GetxController {
110 // } catch (error) { 111 // } catch (error) {
111 // debugPrint('Health authorization skipped: $error'); 112 // debugPrint('Health authorization skipped: $error');
112 // } 113 // }
113 - Get.to(NoHealthDataPage()); 114 + Get.to(NoHealthDataPage(
  115 + onRefresh: () {},
  116 + onHelp: () {
  117 + Get.toNamed(Routes.HELP);
  118 + },
  119 + ));
114 } 120 }
115 121
116 @override 122 @override
@@ -129,8 +135,6 @@ class TodayController extends GetxController { @@ -129,8 +135,6 @@ class TodayController extends GetxController {
129 unawaited(loadDataForDate(today)); 135 unawaited(loadDataForDate(today));
130 } 136 }
131 137
132 - Future<void> loadTodayData() => loadDataForDate(lastSelectableDay);  
133 -  
134 void changeDate(DateTime date, {DateTime? focused}) { 138 void changeDate(DateTime date, {DateTime? focused}) {
135 final normalizedDate = _clampDate(date); 139 final normalizedDate = _clampDate(date);
136 selectedDate.value = normalizedDate; 140 selectedDate.value = normalizedDate;
@@ -148,7 +152,6 @@ class TodayController extends GetxController { @@ -148,7 +152,6 @@ class TodayController extends GetxController {
148 isLoadingToday.value = true; 152 isLoadingToday.value = true;
149 try { 153 try {
150 await Future.wait([ 154 await Future.wait([
151 - _refreshUserGreeting(),  
152 // _refreshHealthAuthorizationState(), 155 // _refreshHealthAuthorizationState(),
153 _refreshHealthDataForDate(date), 156 _refreshHealthDataForDate(date),
154 ]); 157 ]);
@@ -161,16 +164,6 @@ class TodayController extends GetxController { @@ -161,16 +164,6 @@ class TodayController extends GetxController {
161 } 164 }
162 } 165 }
163 166
164 - Future<void> _refreshUserGreeting() async {  
165 - final result = await _userApi.getUserInfo(errorHandlingPolicy: null);  
166 - if (result case AppSuccess<UserInfoResponse>(data: final user)) {  
167 - final name = user.nickname?.trim();  
168 - stressSubtitle.value = name == null || name.isEmpty  
169 - ? 'Hi, 你今日的综合压力状态'  
170 - : 'Hi, $name 今日的综合压力状态';  
171 - }  
172 - }  
173 -  
174 Future<void> _refreshHealthAuthorizationState() async { 167 Future<void> _refreshHealthAuthorizationState() async {
175 final serverAuth = 168 final serverAuth =
176 await _healthApi.checkServerHealthAuth(errorHandlingPolicy: null); 169 await _healthApi.checkServerHealthAuth(errorHandlingPolicy: null);
@@ -197,6 +190,7 @@ class TodayController extends GetxController { @@ -197,6 +190,7 @@ class TodayController extends GetxController {
197 const dateRangeType = 1; 190 const dateRangeType = 1;
198 191
199 final isToday = DateUtils.isSameDay(date, lastSelectableDay); 192 final isToday = DateUtils.isSameDay(date, lastSelectableDay);
  193 + stressSubtitle.value = isToday ? 'Hi, 你今日的综合压力状态' : '该日压力状态';
200 194
201 final todayResultFuture = isToday 195 final todayResultFuture = isToday
202 ? _healthApi.getTodayData( 196 ? _healthApi.getTodayData(
@@ -290,9 +284,8 @@ class TodayController extends GetxController { @@ -290,9 +284,8 @@ class TodayController extends GetxController {
290 stressChartData.clear(); 284 stressChartData.clear();
291 hrvAnnotations.clear(); 285 hrvAnnotations.clear();
292 avgHrv.value = '--'; 286 avgHrv.value = '--';
293 - latestHrvValue.value = '--';  
294 - latestHrvTimeLabel.value = '';  
295 stressLabel.value = '状态正常'; 287 stressLabel.value = '状态正常';
  288 + stressScore.value = null;
296 } 289 }
297 290
298 void _applyTodayData(TodayStatusData today) { 291 void _applyTodayData(TodayStatusData today) {
@@ -331,6 +324,11 @@ class TodayController extends GetxController { @@ -331,6 +324,11 @@ class TodayController extends GetxController {
331 if (latestStatus != null) { 324 if (latestStatus != null) {
332 stressLabel.value = latestStatus.title; 325 stressLabel.value = latestStatus.title;
333 } 326 }
  327 +
  328 + // 更新进度条压力分
  329 + final latestHrvVal = latest?.value ?? average;
  330 + stressScore.value = null; // _computeStressScore(latestHrvVal);
  331 +
334 _updateLatestHrvAnnotation(latest); 332 _updateLatestHrvAnnotation(latest);
335 } 333 }
336 } 334 }
@@ -340,15 +338,14 @@ class TodayController extends GetxController { @@ -340,15 +338,14 @@ class TodayController extends GetxController {
340 if (avgHrv.value == '--' && userHrv != null) { 338 if (avgHrv.value == '--' && userHrv != null) {
341 avgHrv.value = _formatMetric(userHrv); 339 avgHrv.value = _formatMetric(userHrv);
342 } 340 }
343 - if (latestHrvValue.value == '--' && userHrv != null) {  
344 - latestHrvValue.value = _formatMetric(userHrv);  
345 - latestHrvTimeLabel.value = '';  
346 - }  
347 if (userHrv != null) { 341 if (userHrv != null) {
348 stressLabel.value = _hrvStatusTitle( 342 stressLabel.value = _hrvStatusTitle(
349 value: userHrv, 343 value: userHrv,
350 baseline: latestHrv.userHrvBaseline, 344 baseline: latestHrv.userHrvBaseline,
351 ); 345 );
  346 + // 更新进度条压力分
  347 + stressScore.value = null;
  348 + //_computeStressScore(userHrv,baseline: latestHrv.userHrvBaseline,);
352 } 349 }
353 } 350 }
354 351
@@ -484,11 +481,6 @@ class TodayController extends GetxController { @@ -484,11 +481,6 @@ class TodayController extends GetxController {
484 void _applyLatestTodayHrv(TodayHrvData? latest) { 481 void _applyLatestTodayHrv(TodayHrvData? latest) {
485 final value = latest?.value; 482 final value = latest?.value;
486 if (value == null) return; 483 if (value == null) return;
487 -  
488 - latestHrvValue.value = _formatMetric(value);  
489 - final time = latest?.time;  
490 - latestHrvTimeLabel.value =  
491 - time == null ? '' : _formatHour(_hourFromApiTime(time));  
492 } 484 }
493 485
494 void _applySleepDuration(int? rawDuration) { 486 void _applySleepDuration(int? rawDuration) {
@@ -568,6 +560,28 @@ class TodayController extends GetxController { @@ -568,6 +560,28 @@ class TodayController extends GetxController {
568 ]); 560 ]);
569 } 561 }
570 562
  563 + /// 将 HRV 值映射为压力分 0~100(足基线时按相对偏差映射,无基线时按 100-HRV)。
  564 + int _computeStressScore(double hrv, {double? baseline}) {
  565 + if ((baseline ?? 0) > 0) {
  566 + final ratio = hrv / baseline!;
  567 + // ratio≥1.2 → 0~20(优秀)
  568 + if (ratio >= 1.2)
  569 + return ((1.2 - ratio.clamp(1.2, 1.5)) / 0.3 * 20).round().clamp(0, 20);
  570 + // ratio ∈ [0.9,1.2) → 21~50(正常)
  571 + if (ratio >= 0.9)
  572 + return (((1.2 - ratio) / 0.3) * 29 + 21).round().clamp(21, 50);
  573 + // ratio ∈ [0.8,0.9) → 51~80(注意)
  574 + if (ratio >= 0.8)
  575 + return (((0.9 - ratio) / 0.1) * 29 + 51).round().clamp(51, 80);
  576 + // ratio < 0.8 → 81~100(过载)
  577 + return ((0.8 - ratio.clamp(0.4, 0.8)) / 0.4 * 19 + 81)
  578 + .round()
  579 + .clamp(81, 100);
  580 + }
  581 + // 无基线:100 - HRV,HRV 越高 → 分数越低
  582 + return (100 - hrv).clamp(0, 100).round();
  583 + }
  584 +
571 String _hrvStatusTitle({required double value, double? baseline}) { 585 String _hrvStatusTitle({required double value, double? baseline}) {
572 if ((baseline ?? 0) > 0) { 586 if ((baseline ?? 0) > 0) {
573 if (value >= 1.2 * baseline!) return HrvStatus.energetic.title; 587 if (value >= 1.2 * baseline!) return HrvStatus.energetic.title;
@@ -18,6 +18,7 @@ import '../../widgets/today/today_sleep_activity_cards.dart'; @@ -18,6 +18,7 @@ import '../../widgets/today/today_sleep_activity_cards.dart';
18 import '../../widgets/today/stress_status_explanation_bottom_sheet.dart'; 18 import '../../widgets/today/stress_status_explanation_bottom_sheet.dart';
19 19
20 import '../../widgets/today/premium_card.dart'; 20 import '../../widgets/today/premium_card.dart';
  21 +import '../../widgets/today/stress_progress_bar.dart';
21 22
22 class TodayTab extends GetView<TodayController> { 23 class TodayTab extends GetView<TodayController> {
23 const TodayTab({super.key}); 24 const TodayTab({super.key});
@@ -99,6 +100,7 @@ class TodayTab extends GetView<TodayController> { @@ -99,6 +100,7 @@ class TodayTab extends GetView<TodayController> {
99 SliverToBoxAdapter( 100 SliverToBoxAdapter(
100 child: Container( 101 child: Container(
101 height: 323 + pinnedHeaderHeight, 102 height: 323 + pinnedHeaderHeight,
  103 + padding: EdgeInsets.only(bottom: 32),
102 decoration: const BoxDecoration( 104 decoration: const BoxDecoration(
103 gradient: LinearGradient( 105 gradient: LinearGradient(
104 begin: Alignment.topCenter, 106 begin: Alignment.topCenter,
@@ -117,7 +119,9 @@ class TodayTab extends GetView<TodayController> { @@ -117,7 +119,9 @@ class TodayTab extends GetView<TodayController> {
117 Obx( 119 Obx(
118 () => GestureDetector( 120 () => GestureDetector(
119 behavior: HitTestBehavior.opaque, 121 behavior: HitTestBehavior.opaque,
120 - onTap: showStressStatusExplanationBottomSheet, 122 + onTap: () {
  123 + showStressStatusExplanationBottomSheet(context);
  124 + },
121 child: Row( 125 child: Row(
122 mainAxisAlignment: MainAxisAlignment.center, 126 mainAxisAlignment: MainAxisAlignment.center,
123 children: [ 127 children: [
@@ -152,6 +156,12 @@ class TodayTab extends GetView<TodayController> { @@ -152,6 +156,12 @@ class TodayTab extends GetView<TodayController> {
152 ), 156 ),
153 ), 157 ),
154 ), 158 ),
  159 + const SizedBox(height: 12),
  160 + Obx(
  161 + () => StressProgressBar(
  162 + score: controller.stressScore.value,
  163 + ),
  164 + ),
155 ], 165 ],
156 ), 166 ),
157 ), 167 ),
@@ -456,12 +466,7 @@ class _LatestHrvCard extends GetView<TodayController> { @@ -456,12 +466,7 @@ class _LatestHrvCard extends GetView<TodayController> {
456 final colors = context.colors; 466 final colors = context.colors;
457 467
458 return Obx(() { 468 return Obx(() {
459 - final value = controller.latestHrvValue.value;  
460 - final timeLabel = controller.latestHrvTimeLabel.value;  
461 - final hrv = _parseMetric(value);  
462 - final title = timeLabel.isEmpty ? '最近一次' : '最近一次($timeLabel)';  
463 - final statusLabel = hrv == null ? '暂无状态' : _statusLabel(hrv);  
464 - 469 + controller.avgHrv.value;
465 return Container( 470 return Container(
466 padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), 471 padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
467 decoration: BoxDecoration( 472 decoration: BoxDecoration(
@@ -472,7 +477,7 @@ class _LatestHrvCard extends GetView<TodayController> { @@ -472,7 +477,7 @@ class _LatestHrvCard extends GetView<TodayController> {
472 crossAxisAlignment: CrossAxisAlignment.start, 477 crossAxisAlignment: CrossAxisAlignment.start,
473 children: [ 478 children: [
474 Text( 479 Text(
475 - title, 480 + '最近一次(17:20)',
476 style: TextStyle( 481 style: TextStyle(
477 color: colors.textSecondary, 482 color: colors.textSecondary,
478 fontSize: 12, 483 fontSize: 12,
@@ -486,13 +491,13 @@ class _LatestHrvCard extends GetView<TodayController> { @@ -486,13 +491,13 @@ class _LatestHrvCard extends GetView<TodayController> {
486 width: 12, 491 width: 12,
487 height: 12, 492 height: 12,
488 decoration: BoxDecoration( 493 decoration: BoxDecoration(
489 - color: _statusColor(context, hrv), 494 + color: context.colors.chartGreen,
490 shape: BoxShape.circle, 495 shape: BoxShape.circle,
491 ), 496 ),
492 ), 497 ),
493 const SizedBox(width: 8), 498 const SizedBox(width: 8),
494 Text( 499 Text(
495 - '${hrv == null ? '--' : value}ms·$statusLabel', 500 + '66ms·状态优秀',
496 style: TextStyle( 501 style: TextStyle(
497 color: colors.textPrimary, 502 color: colors.textPrimary,
498 fontSize: 18, 503 fontSize: 18,
@@ -503,7 +508,7 @@ class _LatestHrvCard extends GetView<TodayController> { @@ -503,7 +508,7 @@ class _LatestHrvCard extends GetView<TodayController> {
503 ), 508 ),
504 const SizedBox(height: 8), 509 const SizedBox(height: 8),
505 Text( 510 Text(
506 - '暂无最近一次 HRV 数据。完成一次测量后,这里会展示你的最新状态。', 511 + '你的 HRV 高于平时水平。你目前状态放松,压力控制良好——继续保持这样的平衡状态。',
507 maxLines: 2, 512 maxLines: 2,
508 overflow: TextOverflow.ellipsis, 513 overflow: TextOverflow.ellipsis,
509 style: TextStyle( 514 style: TextStyle(
@@ -517,29 +522,6 @@ class _LatestHrvCard extends GetView<TodayController> { @@ -517,29 +522,6 @@ class _LatestHrvCard extends GetView<TodayController> {
517 ); 522 );
518 }); 523 });
519 } 524 }
520 -  
521 - double? _parseMetric(String value) {  
522 - if (value == '--') return null;  
523 - return double.tryParse(value.replaceAll(',', ''));  
524 - }  
525 -  
526 - String _statusLabel(double value) {  
527 - if (value > 60) return '状态优秀';  
528 - if (value > 50) return '状态良好';  
529 - if (value > 30) return '状态一般';  
530 - if (value > 20) return '状态较差';  
531 - return '状态极差';  
532 - }  
533 -  
534 - Color _statusColor(BuildContext context, double? value) {  
535 - final colors = context.colors;  
536 - if (value == null) return colors.textTertiary;  
537 - if (value > 60) return colors.chartGreen;  
538 - if (value > 50) return colors.chartBlue;  
539 - if (value > 30) return colors.chartOrange;  
540 - if (value > 20) return colors.chartPink;  
541 - return colors.chartBlue;  
542 - }  
543 } 525 }
544 526
545 class _TopDateBar extends StatelessWidget { 527 class _TopDateBar extends StatelessWidget {
@@ -5,12 +5,14 @@ import 'package:get/get.dart'; @@ -5,12 +5,14 @@ import 'package:get/get.dart';
5 5
6 import 'today_faq_bottom_sheet.dart'; 6 import 'today_faq_bottom_sheet.dart';
7 7
8 -void showHrvPrincipleExplanationBottomSheet() { 8 +void showHrvPrincipleExplanationBottomSheet(BuildContext context) {
  9 + final screenHeight = MediaQuery.of(context).size.height;
  10 + final statusBarHeight = MediaQuery.of(context).padding.top;
  11 + final maxChildSize = (screenHeight - statusBarHeight) / screenHeight;
9 Get.bottomSheet( 12 Get.bottomSheet(
10 DraggableScrollableSheet( 13 DraggableScrollableSheet(
11 - initialChildSize: 0.96,  
12 - minChildSize: 0.2,  
13 - maxChildSize: 0.96, 14 + maxChildSize: maxChildSize,
  15 + initialChildSize: maxChildSize,
14 expand: false, 16 expand: false,
15 snap: true, 17 snap: true,
16 builder: (context, scrollController) { 18 builder: (context, scrollController) {
@@ -277,7 +279,7 @@ class _FaqLink extends StatelessWidget { @@ -277,7 +279,7 @@ class _FaqLink extends StatelessWidget {
277 Widget build(BuildContext context) { 279 Widget build(BuildContext context) {
278 return GestureDetector( 280 return GestureDetector(
279 behavior: HitTestBehavior.opaque, 281 behavior: HitTestBehavior.opaque,
280 - onTap: showTodayFaqBottomSheet, 282 + onTap: () => showTodayFaqBottomSheet(),
281 child: Padding( 283 child: Padding(
282 padding: const EdgeInsets.only(bottom: 14), 284 padding: const EdgeInsets.only(bottom: 14),
283 child: Text( 285 child: Text(
@@ -229,52 +229,59 @@ class _AuthorizationCard extends StatelessWidget { @@ -229,52 +229,59 @@ class _AuthorizationCard extends StatelessWidget {
229 Widget build(BuildContext context) { 229 Widget build(BuildContext context) {
230 return _CardShell( 230 return _CardShell(
231 child: Column( 231 child: Column(
232 - crossAxisAlignment: CrossAxisAlignment.start,  
233 children: [ 232 children: [
234 - Center(child: _IconPlaceholder()),  
235 - SizedBox(height: 10),  
236 - Text(  
237 - '错误 2:健康数据访问未授权',  
238 - style: TextStyle(  
239 - color: Colors.black,  
240 - fontSize: 16,  
241 - fontWeight: FontWeight.w600,  
242 - height: 22 / 16,  
243 - ),  
244 - ),  
245 - SizedBox(height: 6),  
246 - Text(  
247 - 'DoubleFeel 需要访问 Apple Health 数据的权限,以提供压力统计、提醒和建议。如果未授权,部分功能可能无法正常使用。\n\n请放心,所有健康数据仅存储在本地,不会上传。\n\n要启用权限,请按提示操作,并在 iOS 设置中选择 允许所有 → 健康 → DoubleFeel。',  
248 - style: TextStyle(  
249 - color: context.colors.textSecondary,  
250 - fontSize: 12,  
251 - fontWeight: FontWeight.w400,  
252 - height: 17 / 12,  
253 - ), 233 + Column(
  234 + crossAxisAlignment: CrossAxisAlignment.start,
  235 + mainAxisSize: MainAxisSize.min,
  236 + children: [
  237 + Center(child: _IconPlaceholder()),
  238 + SizedBox(height: 10),
  239 + Text(
  240 + '错误 2:健康数据访问未授权',
  241 + style: TextStyle(
  242 + color: Colors.black,
  243 + fontSize: 16,
  244 + fontWeight: FontWeight.w600,
  245 + height: 22 / 16,
  246 + ),
  247 + ),
  248 + SizedBox(height: 6),
  249 + Text(
  250 + 'DoubleFeel 需要访问 Apple Health 数据的权限,以提供压力统计、提醒和建议。如果未授权,部分功能可能无法正常使用。\n\n请放心,所有健康数据仅存储在本地,不会上传。\n\n要启用权限,请按提示操作,并在 iOS 设置中选择 允许所有 → 健康 → DoubleFeel。',
  251 + style: TextStyle(
  252 + color: context.colors.textSecondary,
  253 + fontSize: 12,
  254 + fontWeight: FontWeight.w400,
  255 + height: 17 / 12,
  256 + ),
  257 + ),
  258 + ],
254 ), 259 ),
255 SizedBox(height: 37), 260 SizedBox(height: 37),
256 Image.asset( 261 Image.asset(
257 - 'assets/images/today/health_data_permission_step_1.png',  
258 - height: 140,  
259 - alignment: Alignment(0, 0.12), 262 + 'assets/images/permission/apple_privacy_cn.png',
  263 + height: 154,
  264 + width: 303,
  265 + ),
  266 + SizedBox(
  267 + height: 10,
260 ), 268 ),
261 - SizedBox(height: 20),  
262 Image.asset( 269 Image.asset(
263 - 'assets/images/today/health_data_permission_step_2.png', 270 + 'assets/images/permission/apple_health_item_cn.png',
264 height: 172, 271 height: 172,
265 - alignment: Alignment(0, 0.24), 272 + width: 303,
266 ), 273 ),
267 SizedBox(height: 20), 274 SizedBox(height: 20),
268 Image.asset( 275 Image.asset(
269 - 'assets/images/today/health_data_permission_step_3.png', 276 + 'assets/images/permission/apple_double_feel_cn.png',
270 height: 210, 277 height: 210,
271 - alignment: Alignment(0, -0.13), 278 + width: 303,
272 ), 279 ),
273 SizedBox(height: 20), 280 SizedBox(height: 20),
274 Image.asset( 281 Image.asset(
275 - 'assets/images/today/health_data_permission_step_4.png', 282 + 'assets/images/permission/apple_health_step_cn.png',
276 height: 167, 283 height: 167,
277 - alignment: Alignment(0, -0.54), 284 + width: 303,
278 ), 285 ),
279 ], 286 ],
280 ), 287 ),
@@ -5,12 +5,14 @@ import 'package:get/get.dart'; @@ -5,12 +5,14 @@ import 'package:get/get.dart';
5 5
6 import 'today_faq_bottom_sheet.dart'; 6 import 'today_faq_bottom_sheet.dart';
7 7
8 -void showRealtimeStressExplanationBottomSheet() { 8 +void showRealtimeStressExplanationBottomSheet(BuildContext context) {
  9 + final screenHeight = MediaQuery.of(context).size.height;
  10 + final statusBarHeight = MediaQuery.of(context).padding.top;
  11 + final maxChildSize = (screenHeight - statusBarHeight) / screenHeight;
9 Get.bottomSheet( 12 Get.bottomSheet(
10 DraggableScrollableSheet( 13 DraggableScrollableSheet(
11 - initialChildSize: 0.96,  
12 - minChildSize: 0.2,  
13 - maxChildSize: 0.96, 14 + maxChildSize: maxChildSize,
  15 + initialChildSize: maxChildSize,
14 expand: false, 16 expand: false,
15 snap: true, 17 snap: true,
16 builder: (context, scrollController) { 18 builder: (context, scrollController) {
@@ -309,7 +311,7 @@ class _FaqLink extends StatelessWidget { @@ -309,7 +311,7 @@ class _FaqLink extends StatelessWidget {
309 Widget build(BuildContext context) { 311 Widget build(BuildContext context) {
310 return GestureDetector( 312 return GestureDetector(
311 behavior: HitTestBehavior.opaque, 313 behavior: HitTestBehavior.opaque,
312 - onTap: showTodayFaqBottomSheet, 314 + onTap: () => showTodayFaqBottomSheet(),
313 child: Padding( 315 child: Padding(
314 padding: const EdgeInsets.only(bottom: 14), 316 padding: const EdgeInsets.only(bottom: 14),
315 child: Text( 317 child: Text(
  1 +import 'package:flutter/material.dart';
  2 +
  3 +/// 压力状态进度条(四段式)
  4 +///
  5 +/// 对应 Figma node 1361-44236(轨道)和 1361-44489(indicator)。
  6 +///
  7 +/// 传入 [score](0~100 压力分):
  8 +/// - null → 加载态:四段等宽 20px,无 indicator
  9 +/// - 0~20 → 优秀·绿段(status 3)
  10 +/// - 21~50 → 正常·蓝段(status 2)
  11 +/// - 51~80 → 注意压力·橙段(status 1)
  12 +/// - 81~100 → 压力过载·红段(status 0)
  13 +///
  14 +/// indicator 在命中段内的横向位置由分数在该区间的位置决定:
  15 +/// 分数越低(越优秀)→ 指针越靠右;分数越高(越差)→ 越靠左。
  16 +///
  17 +/// 段间距 4px,轨道高 14px,indicator 24×24px(6px stroke)。
  18 +class StressProgressBar extends StatelessWidget {
  19 + const StressProgressBar({super.key, required this.score})
  20 + : assert(score == null || (score >= 0 && score <= 100));
  21 +
  22 + /// null = 加载中;0~100 = 压力分
  23 + final int? score;
  24 +
  25 + // ── Figma 图表色/1-4(index 0~3 对应 过载→活力)────────
  26 + static const _colors = [
  27 + Color(0xFFFF5279), // 0 压力过载·红
  28 + Color(0xFFFF9A6E), // 1 注意压力·橙
  29 + Color(0xFF7B9BFB), // 2 状态正常·蓝
  30 + Color(0xFF3BD49D), // 3 活力满满·绿
  31 + ];
  32 +
  33 + // ── 尺寸常量(来自 Figma)────────────────────────────
  34 + static const _defaultW = 20.0;
  35 + static const _activeW = 148.0;
  36 + static const _trackH = 14.0;
  37 + static const _gap = 4.0;
  38 + static const _dotSize = 24.0;
  39 + static const _dotBorder = 6.0;
  40 +
  41 + // ── 动画参数 ──────────────────────────────────────────
  42 + static const _dur = Duration(milliseconds: 400);
  43 + static const _curve = Curves.easeInOut;
  44 +
  45 + // ── 分数区间边界(左闭右闭)───────────────────────────
  46 + // status 3 (绿): 0~20 status 2 (蓝): 21~50
  47 + // status 1 (橙): 51~80 status 0 (红): 81~100
  48 + static const _ranges = [
  49 + (lo: 81, hi: 100), // status 0
  50 + (lo: 51, hi: 80), // status 1
  51 + (lo: 21, hi: 50), // status 2
  52 + (lo: 0, hi: 20), // status 3
  53 + ];
  54 +
  55 + /// 由压力分计算命中段(0=过载, 1=注意, 2=正常, 3=活力)
  56 + static int _statusFromScore(int s) {
  57 + if (s <= 20) return 3;
  58 + if (s <= 50) return 2;
  59 + if (s <= 80) return 1;
  60 + return 0;
  61 + }
  62 +
  63 + /// 在命中段内的归一化位置(0.0=最左/最差, 1.0=最右/最优)
  64 + ///
  65 + /// 分数越低越优秀 → 越靠右,所以 progress = (hi - score) / (hi - lo)
  66 + static double _segProgress(int s) {
  67 + final st = _statusFromScore(s);
  68 + final r = _ranges[st];
  69 + if (r.hi == r.lo) return 0.5;
  70 + return ((r.hi - s) / (r.hi - r.lo)).clamp(0.0, 1.0);
  71 + }
  72 +
  73 + // ── 布局计算 ──────────────────────────────────────────
  74 +
  75 + int? get _status => score != null ? _statusFromScore(score!) : null;
  76 +
  77 + /// 第 [i] 段的当前宽度
  78 + double _w(int i) => (_status == i) ? _activeW : _defaultW;
  79 +
  80 + /// 总宽度(含三个间距)
  81 + double get _totalW => _w(0) + _w(1) + _w(2) + _w(3) + 3 * _gap;
  82 +
  83 + /// indicator 左边缘 X(在命中段内按 segProgress 定位)
  84 + double _dotLeft(int s) {
  85 + final prog = score != null ? _segProgress(score!) : 0.5;
  86 + double x = 0;
  87 + for (int i = 0; i < s; i++) {
  88 + x += _w(i) + _gap;
  89 + }
  90 + // 在命中段宽内按 progress 定位,并保证圆点不超出段的边缘
  91 + x += (_w(s) * prog).clamp(_dotSize / 2, _w(s) - _dotSize / 2);
  92 + return x - _dotSize / 2;
  93 + }
  94 +
  95 + @override
  96 + Widget build(BuildContext context) {
  97 + final s = _status;
  98 +
  99 + return AnimatedContainer(
  100 + duration: _dur,
  101 + curve: _curve,
  102 + width: _totalW,
  103 + height: _dotSize,
  104 + child: Stack(
  105 + clipBehavior: Clip.none,
  106 + children: [
  107 + // ── 轨道:四段色块 ─────────────────────────────
  108 + Positioned(
  109 + top: (_dotSize - _trackH) / 2,
  110 + left: 0,
  111 + child: Row(
  112 + mainAxisSize: MainAxisSize.min,
  113 + children: [
  114 + for (int i = 0; i < 4; i++) ...[
  115 + AnimatedContainer(
  116 + duration: _dur,
  117 + curve: _curve,
  118 + width: _w(i),
  119 + height: _trackH,
  120 + decoration: BoxDecoration(
  121 + color: _colors[i],
  122 + borderRadius: BorderRadius.circular(_trackH / 2),
  123 + ),
  124 + ),
  125 + if (i < 3) const SizedBox(width: _gap),
  126 + ],
  127 + ],
  128 + ),
  129 + ),
  130 +
  131 + // ── Indicator:白底 + 状态色描边 ────────────────
  132 + AnimatedPositioned(
  133 + duration: _dur,
  134 + curve: _curve,
  135 + top: 0,
  136 + left: s != null ? _dotLeft(s) : 0,
  137 + child: AnimatedOpacity(
  138 + opacity: s != null ? 1.0 : 0.0,
  139 + duration: _dur,
  140 + curve: _curve,
  141 + child: Container(
  142 + width: _dotSize,
  143 + height: _dotSize,
  144 + decoration: BoxDecoration(
  145 + shape: BoxShape.circle,
  146 + color: Colors.white,
  147 + border: Border.all(
  148 + color: _colors[(s ?? 0).clamp(0, 3)],
  149 + width: _dotBorder,
  150 + ),
  151 + ),
  152 + ),
  153 + ),
  154 + ),
  155 + ],
  156 + ),
  157 + );
  158 + }
  159 +}
@@ -5,12 +5,14 @@ import 'package:get/get.dart'; @@ -5,12 +5,14 @@ import 'package:get/get.dart';
5 5
6 import 'today_faq_bottom_sheet.dart'; 6 import 'today_faq_bottom_sheet.dart';
7 7
8 -void showStressStatusExplanationBottomSheet() { 8 +void showStressStatusExplanationBottomSheet(BuildContext context) {
  9 + final screenHeight = MediaQuery.of(context).size.height;
  10 + final statusBarHeight = MediaQuery.of(context).padding.top;
  11 + final maxChildSize = (screenHeight - statusBarHeight) / screenHeight;
9 Get.bottomSheet( 12 Get.bottomSheet(
10 DraggableScrollableSheet( 13 DraggableScrollableSheet(
11 - initialChildSize: 0.96,  
12 - minChildSize: 0.2,  
13 - maxChildSize: 0.96, 14 + maxChildSize: maxChildSize,
  15 + initialChildSize: maxChildSize,
14 expand: false, 16 expand: false,
15 snap: true, 17 snap: true,
16 builder: (context, scrollController) { 18 builder: (context, scrollController) {
@@ -43,6 +45,9 @@ class StressStatusExplanationBottomSheet extends StatelessWidget { @@ -43,6 +45,9 @@ class StressStatusExplanationBottomSheet extends StatelessWidget {
43 color: Color(0xFFF5F2FF), 45 color: Color(0xFFF5F2FF),
44 borderRadius: BorderRadius.vertical(top: Radius.circular(16)), 46 borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
45 ), 47 ),
  48 + padding: EdgeInsets.only(
  49 + top: MediaQuery.of(context).padding.top,
  50 + ),
46 child: SafeArea( 51 child: SafeArea(
47 top: false, 52 top: false,
48 child: Column( 53 child: Column(
@@ -307,7 +312,7 @@ class _FaqLink extends StatelessWidget { @@ -307,7 +312,7 @@ class _FaqLink extends StatelessWidget {
307 Widget build(BuildContext context) { 312 Widget build(BuildContext context) {
308 return GestureDetector( 313 return GestureDetector(
309 behavior: HitTestBehavior.opaque, 314 behavior: HitTestBehavior.opaque,
310 - onTap: showTodayFaqBottomSheet, 315 + onTap: () => showTodayFaqBottomSheet(),
311 child: Padding( 316 child: Padding(
312 padding: const EdgeInsets.only(bottom: 14), 317 padding: const EdgeInsets.only(bottom: 14),
313 child: Text( 318 child: Text(
1 -import 'package:flutter/material.dart';  
2 -import 'package:get/get.dart';  
3 -import '../../controllers/today_controller.dart';  
4 -  
5 -/// Figma: 横向 7 日日期条,当前选中日高亮品牌色圆角背景  
6 -/// 支持左右滑动切换周次(PageView)  
7 -class TodayDateStrip extends GetView<TodayController> {  
8 - const TodayDateStrip({super.key});  
9 -  
10 - static const _brandColor = Color(0xFF845EEE);  
11 - static const _h1Color = Color(0xFF0F0F11);  
12 - static const _h5Color = Color(0xFFCCCCCC);  
13 -  
14 - /// 获取以本周一为起点的 7 天列表  
15 - List<DateTime> _getWeekDays(DateTime base) {  
16 - final monday = base.subtract(Duration(days: base.weekday - 1));  
17 - return List.generate(7, (i) => monday.add(Duration(days: i)));  
18 - }  
19 -  
20 - String _weekdayLabel(int weekday) {  
21 - const labels = ['一', '二', '三', '四', '五', '六', '日'];  
22 - return labels[weekday - 1];  
23 - }  
24 -  
25 - @override  
26 - Widget build(BuildContext context) {  
27 - return Obx(() {  
28 - final selected = controller.selectedDate.value;  
29 - final today = DateTime.now();  
30 - final days = _getWeekDays(selected);  
31 -  
32 - return Container(  
33 - height: 72,  
34 - decoration: BoxDecoration(  
35 - color: _brandColor,  
36 - borderRadius: BorderRadius.circular(22),  
37 - ),  
38 - child: Row(  
39 - children: days.map((day) {  
40 - final isSelected = _isSameDay(day, selected);  
41 - final isToday = _isSameDay(day, today);  
42 - final isFuture = day.isAfter(today);  
43 -  
44 - return Expanded(  
45 - child: GestureDetector(  
46 - onTap: isFuture ? null : () => controller.changeDate(day),  
47 - child: Column(  
48 - mainAxisAlignment: MainAxisAlignment.center,  
49 - children: [  
50 - // 周几  
51 - Text(  
52 - _weekdayLabel(day.weekday),  
53 - style: TextStyle(  
54 - fontSize: 11,  
55 - fontWeight: FontWeight.w400,  
56 - color: isFuture  
57 - ? _h5Color  
58 - : (isSelected ? _h1Color : Colors.white70),  
59 - ),  
60 - ),  
61 - const SizedBox(height: 4),  
62 - // 日期数字  
63 - AnimatedContainer(  
64 - duration: const Duration(milliseconds: 180),  
65 - curve: Curves.easeOut,  
66 - width: 28,  
67 - height: 28,  
68 - decoration: BoxDecoration(  
69 - color: isSelected ? Colors.white : Colors.transparent,  
70 - borderRadius: BorderRadius.circular(14),  
71 - ),  
72 - alignment: Alignment.center,  
73 - child: Text(  
74 - '${day.day}',  
75 - style: TextStyle(  
76 - fontSize: 15,  
77 - fontWeight: isSelected || isToday  
78 - ? FontWeight.w600  
79 - : FontWeight.w400,  
80 - color: isSelected  
81 - ? _brandColor  
82 - : isFuture  
83 - ? _h5Color  
84 - : Colors.white,  
85 - ),  
86 - ),  
87 - ),  
88 - ],  
89 - ),  
90 - ),  
91 - );  
92 - }).toList(),  
93 - ),  
94 - );  
95 - });  
96 - }  
97 -  
98 - bool _isSameDay(DateTime a, DateTime b) {  
99 - return a.year == b.year && a.month == b.month && a.day == b.day;  
100 - }  
101 -}  
@@ -4,11 +4,15 @@ import 'package:flutter/material.dart'; @@ -4,11 +4,15 @@ import 'package:flutter/material.dart';
4 import 'package:get/get.dart'; 4 import 'package:get/get.dart';
5 5
6 void showTodayFaqBottomSheet() { 6 void showTodayFaqBottomSheet() {
  7 + final view = WidgetsBinding.instance.platformDispatcher.views.first;
  8 + final devicePixelRatio = view.devicePixelRatio;
  9 + final screenHeight = view.physicalSize.height / devicePixelRatio;
  10 + final statusBarHeight = view.padding.top / devicePixelRatio;
  11 + final maxChildSize = (screenHeight - statusBarHeight) / screenHeight;
7 Get.bottomSheet( 12 Get.bottomSheet(
8 DraggableScrollableSheet( 13 DraggableScrollableSheet(
9 - initialChildSize: 0.96,  
10 - minChildSize: 0.2,  
11 - maxChildSize: 0.96, 14 + maxChildSize: maxChildSize,
  15 + initialChildSize: maxChildSize,
12 expand: false, 16 expand: false,
13 snap: true, 17 snap: true,
14 builder: (context, scrollController) { 18 builder: (context, scrollController) {
  1 +import 'package:doublefeel_flutter/app/routes/app_pages.dart';
1 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 2 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
2 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 3 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
3 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 4 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
@@ -65,7 +66,8 @@ class TodayHrvChartCard extends GetView<TodayController> { @@ -65,7 +66,8 @@ class TodayHrvChartCard extends GetView<TodayController> {
65 padding: const EdgeInsets.symmetric(horizontal: 2), 66 padding: const EdgeInsets.symmetric(horizontal: 2),
66 child: GestureDetector( 67 child: GestureDetector(
67 behavior: HitTestBehavior.opaque, 68 behavior: HitTestBehavior.opaque,
68 - onTap: showRealtimeStressExplanationBottomSheet, 69 + onTap: () =>
  70 + showRealtimeStressExplanationBottomSheet(context),
69 child: Image.asset( 71 child: Image.asset(
70 'assets/images/common/ic_pro_badge.webp', 72 'assets/images/common/ic_pro_badge.webp',
71 width: 43, 73 width: 43,
@@ -77,7 +79,8 @@ class TodayHrvChartCard extends GetView<TodayController> { @@ -77,7 +79,8 @@ class TodayHrvChartCard extends GetView<TodayController> {
77 padding: const EdgeInsets.symmetric(horizontal: 2), 79 padding: const EdgeInsets.symmetric(horizontal: 2),
78 child: GestureDetector( 80 child: GestureDetector(
79 behavior: HitTestBehavior.opaque, 81 behavior: HitTestBehavior.opaque,
80 - onTap: showRealtimeStressExplanationBottomSheet, 82 + onTap: () =>
  83 + showRealtimeStressExplanationBottomSheet(context),
81 child: Image.asset( 84 child: Image.asset(
82 'assets/images/common/ic_info.png', 85 'assets/images/common/ic_info.png',
83 width: 14, 86 width: 14,
@@ -91,7 +94,7 @@ class TodayHrvChartCard extends GetView<TodayController> { @@ -91,7 +94,7 @@ class TodayHrvChartCard extends GetView<TodayController> {
91 ), 94 ),
92 ), 95 ),
93 SizedBox( 96 SizedBox(
94 - height: 174, 97 + height: 190,
95 child: stressPoints.isEmpty 98 child: stressPoints.isEmpty
96 ? const _EmptyChart(text: '暂无压力数据') 99 ? const _EmptyChart(text: '暂无压力数据')
97 : _buildStressChart( 100 : _buildStressChart(
@@ -117,7 +120,7 @@ class TodayHrvChartCard extends GetView<TodayController> { @@ -117,7 +120,7 @@ class TodayHrvChartCard extends GetView<TodayController> {
117 const SizedBox(width: 4), 120 const SizedBox(width: 4),
118 GestureDetector( 121 GestureDetector(
119 behavior: HitTestBehavior.opaque, 122 behavior: HitTestBehavior.opaque,
120 - onTap: showHrvPrincipleExplanationBottomSheet, 123 + onTap: () => showHrvPrincipleExplanationBottomSheet(context),
121 child: Image.asset( 124 child: Image.asset(
122 'assets/images/common/ic_info.png', 125 'assets/images/common/ic_info.png',
123 width: 14, 126 width: 14,
@@ -253,130 +256,218 @@ class TodayHrvChartCard extends GetView<TodayController> { @@ -253,130 +256,218 @@ class TodayHrvChartCard extends GetView<TodayController> {
253 256
254 Widget _buildStressChart( 257 Widget _buildStressChart(
255 BuildContext context, List<HrvDataPoint> stressPoints, bool isVip) { 258 BuildContext context, List<HrvDataPoint> stressPoints, bool isVip) {
  259 + void onTapPurchase() => Get.toNamed(Routes.PURCHASE);
  260 + final spanWidth = 74.0;
256 return Stack( 261 return Stack(
257 children: [ 262 children: [
258 - BarChart(  
259 - BarChartData(  
260 - minY: 0,  
261 - maxY: 100,  
262 - groupsSpace: 1,  
263 - alignment: BarChartAlignment.start,  
264 - barGroups: List.generate(  
265 - stressPoints.length,  
266 - (i) {  
267 - final point = stressPoints[i];  
268 - return BarChartGroupData(  
269 - x: (point.hour * 10).round(),  
270 - barRods: [  
271 - BarChartRodData(  
272 - toY: point.hrv,  
273 - width: 2,  
274 - color: _getColor(point.hrv),  
275 - borderRadius: const BorderRadius.only(  
276 - topLeft: Radius.circular(2),  
277 - topRight: Radius.circular(2), 263 + Padding(
  264 + padding: const EdgeInsets.only(top: 6),
  265 + child: BarChart(
  266 + BarChartData(
  267 + minY: 0,
  268 + maxY: 100,
  269 + groupsSpace: 1,
  270 + alignment: BarChartAlignment.start,
  271 + barGroups: List.generate(
  272 + stressPoints.length,
  273 + (i) {
  274 + final point = stressPoints[i];
  275 + return BarChartGroupData(
  276 + x: (point.hour * 10).round(),
  277 + barRods: [
  278 + BarChartRodData(
  279 + toY: point.hrv,
  280 + width: 2,
  281 + color: _getColor(point.hrv),
  282 + borderRadius: const BorderRadius.only(
  283 + topLeft: Radius.circular(2),
  284 + topRight: Radius.circular(2),
  285 + ),
278 ), 286 ),
279 - ),  
280 - ],  
281 - );  
282 - },  
283 - ),  
284 - gridData: FlGridData(  
285 - show: true,  
286 - drawVerticalLine: false,  
287 - drawHorizontalLine: true,  
288 - horizontalInterval: 25,  
289 - getDrawingHorizontalLine: (_) => const FlLine(  
290 - color: _h5,  
291 - strokeWidth: 1,  
292 - dashArray: [2, 2], 287 + ],
  288 + );
  289 + },
293 ), 290 ),
294 - ),  
295 - borderData: FlBorderData(show: false),  
296 - titlesData: FlTitlesData(  
297 - topTitles:  
298 - const AxisTitles(sideTitles: SideTitles(showTitles: false)),  
299 - rightTitles: AxisTitles(  
300 - sideTitles: SideTitles(  
301 - showTitles: true,  
302 - getTitlesWidget: (val, _) {  
303 - if (val < 0 || val > 100) {  
304 - return const SizedBox.shrink();  
305 - }  
306 - return Text(  
307 - '${val.toInt()}',  
308 - style: TextStyle(  
309 - fontSize: 10, color: context.colors.textTertiary),  
310 - );  
311 - }, 291 + gridData: FlGridData(
  292 + show: true,
  293 + drawVerticalLine: false,
  294 + drawHorizontalLine: true,
  295 + horizontalInterval: 25,
  296 + getDrawingHorizontalLine: (_) => const FlLine(
  297 + color: _h5,
  298 + strokeWidth: 1,
  299 + dashArray: [2, 2],
312 ), 300 ),
313 ), 301 ),
314 - leftTitles:  
315 - const AxisTitles(sideTitles: SideTitles(showTitles: false)),  
316 - bottomTitles: AxisTitles(  
317 - sideTitles: SideTitles(  
318 - showTitles: true,  
319 - reservedSize: 20,  
320 - interval: 60,  
321 - getTitlesWidget: (val, meta) {  
322 - final label = _timeLabel(val / 10);  
323 - if (label == null) return const SizedBox.shrink();  
324 - return SideTitleWidget(  
325 - meta: meta,  
326 - child: Text(  
327 - label, 302 + borderData: FlBorderData(show: false),
  303 + titlesData: FlTitlesData(
  304 + topTitles:
  305 + const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  306 + leftTitles:
  307 + const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  308 + rightTitles: AxisTitles(
  309 + sideTitles: SideTitles(
  310 + showTitles: true,
  311 + interval: 50,
  312 + reservedSize: 22,
  313 + getTitlesWidget: (val, _) {
  314 + if (val < 0 || val > 100) {
  315 + return const SizedBox.shrink();
  316 + }
  317 + return Text(
  318 + '${val.toInt()}',
328 style: TextStyle( 319 style: TextStyle(
329 fontSize: 10, color: context.colors.textTertiary), 320 fontSize: 10, color: context.colors.textTertiary),
  321 + );
  322 + },
  323 + ),
  324 + ),
  325 + bottomTitles: AxisTitles(
  326 + sideTitles: SideTitles(
  327 + showTitles: true,
  328 + reservedSize: 22,
  329 + interval: 60,
  330 + getTitlesWidget: (val, meta) {
  331 + final label = _timeLabel(val / 10);
  332 + if (label == null) return const SizedBox.shrink();
  333 + return SideTitleWidget(
  334 + meta: meta,
  335 + child: Text(
  336 + label,
  337 + style: TextStyle(
  338 + fontSize: 10, color: context.colors.textTertiary),
  339 + ),
  340 + );
  341 + },
  342 + ),
  343 + ),
  344 + ),
  345 + barTouchData: BarTouchData(
  346 + touchTooltipData: BarTouchTooltipData(
  347 + tooltipRoundedRadius: 8,
  348 + tooltipBorder: BorderSide.none,
  349 + getTooltipColor: (touchedSpot) => const Color(0xFFF3F3F3),
  350 + tooltipPadding: const EdgeInsets.only(
  351 + left: 12,
  352 + right: 12,
  353 + top: 6,
  354 + bottom: 5,
  355 + ),
  356 + getTooltipItem: (group, groupIndex, rod, rodIndex) {
  357 + return BarTooltipItem(
  358 + '',
  359 + const TextStyle(
  360 + color: Colors.white,
  361 + fontWeight: FontWeight.bold,
330 ), 362 ),
  363 + children: _getTooltip(rod),
  364 + textAlign: TextAlign.start,
331 ); 365 );
332 }, 366 },
333 ), 367 ),
334 ), 368 ),
335 ), 369 ),
336 - barTouchData: BarTouchData(  
337 - touchTooltipData: BarTouchTooltipData(  
338 - tooltipRoundedRadius: 8,  
339 - tooltipBorder: BorderSide.none,  
340 - getTooltipColor: (touchedSpot) => const Color(0xFFF3F3F3),  
341 - tooltipPadding: const EdgeInsets.only(  
342 - left: 12,  
343 - right: 12,  
344 - top: 6,  
345 - bottom: 5,  
346 - ),  
347 - getTooltipItem: (group, groupIndex, rod, rodIndex) {  
348 - return BarTooltipItem(  
349 - '',  
350 - const TextStyle(  
351 - color: Colors.white,  
352 - fontWeight: FontWeight.bold,  
353 - ),  
354 - children: _getTooltip(rod),  
355 - textAlign: TextAlign.start,  
356 - );  
357 - }, 370 + ),
  371 + ),
  372 + Positioned(
  373 + left: 0,
  374 + top: 14,
  375 + bottom: 22,
  376 + child: Container(
  377 + width: spanWidth,
  378 + height: 174,
  379 + decoration: const BoxDecoration(
  380 + gradient: LinearGradient(
  381 + begin: Alignment(0.50, -0.00),
  382 + end: Alignment(0.50, 1.00),
  383 + colors: [Color(0x4C835DED), Color(0x00845EEE)],
358 ), 384 ),
359 ), 385 ),
360 ), 386 ),
361 ), 387 ),
362 - Container(  
363 - width: 74,  
364 - height: 174,  
365 - decoration: const BoxDecoration(  
366 - gradient: LinearGradient(  
367 - begin: Alignment(0.50, -0.00),  
368 - end: Alignment(0.50, 1.00),  
369 - colors: [Color(0x4C835DED), Color(0x00845EEE)],  
370 - ), 388 + Positioned(
  389 + left: 0,
  390 + top: 14,
  391 + width: spanWidth,
  392 + height: 2,
  393 + child: ColoredBox(color: context.colors.primary),
  394 + ),
  395 + Positioned(
  396 + left: spanWidth / 2 - 6,
  397 + top: 0,
  398 + child: Image.asset(
  399 + 'assets/images/common/ic_health_bed.webp',
  400 + width: 12,
  401 + height: 12,
371 ), 402 ),
372 ), 403 ),
373 - if (isVip == false)  
374 - Image.asset(  
375 - 'assets/images/today/bg_unlock_instant_stress_blur.png',  
376 - width: double.infinity,  
377 - height: 174,  
378 - fit: BoxFit.cover,  
379 - ) 404 + if (isVip == false) ...[
  405 + GestureDetector(
  406 + onTap: onTapPurchase,
  407 + child: Image.asset(
  408 + 'assets/images/today/bg_unlock_instant_stress_blur.png',
  409 + width: double.infinity,
  410 + height: 174,
  411 + fit: BoxFit.cover,
  412 + ),
  413 + ),
  414 + Positioned(
  415 + top: 97,
  416 + left: 0,
  417 + right: 0,
  418 + child: Column(
  419 + mainAxisSize: MainAxisSize.min,
  420 + crossAxisAlignment: CrossAxisAlignment.center,
  421 + children: [
  422 + Row(
  423 + mainAxisSize: MainAxisSize.max,
  424 + mainAxisAlignment: MainAxisAlignment.center,
  425 + children: [
  426 + Image.asset(
  427 + 'assets/images/common/ic_lock.png',
  428 + width: 12,
  429 + height: 12,
  430 + color: context.colors.textTertiary,
  431 + ),
  432 + Text(
  433 + '会员可查看完整数据',
  434 + style: TextStyle(
  435 + color: context.colors.textTertiary,
  436 + fontSize: 14,
  437 + fontWeight: FontWeight.w400,
  438 + ),
  439 + ),
  440 + ],
  441 + ),
  442 + GestureDetector(
  443 + onTap: onTapPurchase,
  444 + child: Container(
  445 + constraints: const BoxConstraints(
  446 + minWidth: 120,
  447 + ),
  448 + margin: const EdgeInsets.only(top: 16),
  449 + padding: const EdgeInsets.symmetric(
  450 + horizontal: 16, vertical: 8),
  451 + decoration: ShapeDecoration(
  452 + color: const Color(0xFFFFDF50),
  453 + shape: RoundedRectangleBorder(
  454 + borderRadius: BorderRadius.circular(24),
  455 + ),
  456 + ),
  457 + child: const Text(
  458 + '立即解锁',
  459 + textAlign: TextAlign.center,
  460 + style: TextStyle(
  461 + color: Color(0xFF0F0F11),
  462 + fontSize: 14,
  463 + fontWeight: FontWeight.w500,
  464 + ),
  465 + ),
  466 + ),
  467 + )
  468 + ]),
  469 + ),
  470 + ]
380 ], 471 ],
381 ); 472 );
382 } 473 }
  1 +import 'package:doublefeel_flutter/core/theme/app_theme.dart';
1 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 2 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
2 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
3 import 'package:get/get.dart'; 4 import 'package:get/get.dart';
@@ -51,9 +52,6 @@ class _NumberItem extends StatelessWidget { @@ -51,9 +52,6 @@ class _NumberItem extends StatelessWidget {
51 required this.unit, 52 required this.unit,
52 }); 53 });
53 54
54 - static const _h1 = Color(0xFF0F0F11);  
55 - static const _h2 = Color(0xFF666666);  
56 -  
57 @override 55 @override
58 Widget build(BuildContext context) { 56 Widget build(BuildContext context) {
59 return Padding( 57 return Padding(
@@ -63,9 +61,9 @@ class _NumberItem extends StatelessWidget { @@ -63,9 +61,9 @@ class _NumberItem extends StatelessWidget {
63 children: [ 61 children: [
64 Text( 62 Text(
65 label, 63 label,
66 - style: const TextStyle( 64 + style: TextStyle(
67 fontSize: 12, 65 fontSize: 12,
68 - color: _h2, 66 + color: context.colors.textTertiary,
69 fontWeight: FontWeight.w400, 67 fontWeight: FontWeight.w400,
70 ), 68 ),
71 ), 69 ),
@@ -75,10 +73,10 @@ class _NumberItem extends StatelessWidget { @@ -75,10 +73,10 @@ class _NumberItem extends StatelessWidget {
75 children: [ 73 children: [
76 Text( 74 Text(
77 value, 75 value,
78 - style: const TextStyle( 76 + style: TextStyle(
79 fontSize: 28, 77 fontSize: 28,
80 fontWeight: FontWeight.w700, 78 fontWeight: FontWeight.w700,
81 - color: _h1, 79 + color: context.colors.textPrimary,
82 height: 1, 80 height: 1,
83 ), 81 ),
84 ), 82 ),
@@ -87,9 +85,9 @@ class _NumberItem extends StatelessWidget { @@ -87,9 +85,9 @@ class _NumberItem extends StatelessWidget {
87 padding: const EdgeInsets.only(bottom: 3), 85 padding: const EdgeInsets.only(bottom: 3),
88 child: Text( 86 child: Text(
89 unit, 87 unit,
90 - style: const TextStyle( 88 + style: TextStyle(
91 fontSize: 13, 89 fontSize: 13,
92 - color: _h1, 90 + color: context.colors.textPrimary,
93 fontWeight: FontWeight.w400, 91 fontWeight: FontWeight.w400,
94 ), 92 ),
95 ), 93 ),
  1 +import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/combine.dart';
1 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 2 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
2 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 3 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
3 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
@@ -46,6 +47,26 @@ class TodaySleepCard extends GetView<TodayController> { @@ -46,6 +47,26 @@ class TodaySleepCard extends GetView<TodayController> {
46 ], 47 ],
47 ), 48 ),
48 ], 49 ],
  50 + rightWidget: Container(
  51 + width: 50,
  52 + height: 50,
  53 + margin: EdgeInsets.only(right: 6, left: 4),
  54 + child: CircularGradientCombineWidget(
  55 + size: 50,
  56 + duration: const Duration(seconds: 1),
  57 + sweepAngles: const [240, 210],
  58 + backgroundColors: const [
  59 + Color(0xFFF3F3F3),
  60 + Color(0xFFF3F3F3),
  61 + ],
  62 + gradientColors: [
  63 + [context.colors.primary],
  64 + [context.colors.chartBlue],
  65 + ],
  66 + centerCircleSizeRatio: 0.3,
  67 + gapRatio: 0.1,
  68 + interpolatedColorRatio: 1),
  69 + ),
49 ), 70 ),
50 ); 71 );
51 } 72 }
@@ -108,6 +129,48 @@ class TodayActivityCard extends GetView<TodayController> { @@ -108,6 +129,48 @@ class TodayActivityCard extends GetView<TodayController> {
108 ], 129 ],
109 ), 130 ),
110 ], 131 ],
  132 + rightWidget: Container(
  133 + width: 50,
  134 + height: 50,
  135 + margin: EdgeInsets.only(right: 6, left: 4),
  136 + child: Stack(
  137 + children: [
  138 + Container(
  139 + decoration: BoxDecoration(
  140 + shape: BoxShape.circle,
  141 + color: context.colors.textPrimary,
  142 + ),
  143 + ),
  144 + CircularGradientCombineWidget(
  145 + size: 50,
  146 + duration: const Duration(seconds: 1),
  147 + centerCircleSizeRatio: 0.2,
  148 + gapRatio: 0.06,
  149 + sweepAngles: const [
  150 + 180,
  151 + 230,
  152 + 180,
  153 + ],
  154 + backgroundColors: [
  155 + context.colors.chartPink.withOpacity(0.4),
  156 + context.colors.chartGreen.withOpacity(0.3),
  157 + context.colors.chartBlue.withOpacity(0.4),
  158 + ],
  159 + gradientColors: [
  160 + [
  161 + context.colors.chartPink,
  162 + ],
  163 + [
  164 + context.colors.chartGreen,
  165 + ],
  166 + [
  167 + context.colors.chartBlue,
  168 + ],
  169 + ],
  170 + ),
  171 + ],
  172 + ),
  173 + ),
111 ), 174 ),
112 ); 175 );
113 } 176 }
@@ -122,6 +185,7 @@ class _TodaySummaryCard extends StatelessWidget { @@ -122,6 +185,7 @@ class _TodaySummaryCard extends StatelessWidget {
122 required this.metrics, 185 required this.metrics,
123 required this.progressValues, 186 required this.progressValues,
124 required this.progressColors, 187 required this.progressColors,
  188 + required this.rightWidget,
125 }); 189 });
126 190
127 final String iconAsset; 191 final String iconAsset;
@@ -131,6 +195,7 @@ class _TodaySummaryCard extends StatelessWidget { @@ -131,6 +195,7 @@ class _TodaySummaryCard extends StatelessWidget {
131 final List<_MetricData> metrics; 195 final List<_MetricData> metrics;
132 final List<double> progressValues; 196 final List<double> progressValues;
133 final List<Color> progressColors; 197 final List<Color> progressColors;
  198 + final Widget rightWidget;
134 199
135 @override 200 @override
136 Widget build(BuildContext context) { 201 Widget build(BuildContext context) {
@@ -194,13 +259,7 @@ class _TodaySummaryCard extends StatelessWidget { @@ -194,13 +259,7 @@ class _TodaySummaryCard extends StatelessWidget {
194 ], 259 ],
195 ), 260 ),
196 ), 261 ),
197 - const SizedBox(width: 4),  
198 - Container(  
199 - width: 50,  
200 - height: 50,  
201 - color: Colors.red,  
202 - ),  
203 - const SizedBox(width: 6), 262 + rightWidget
204 ], 263 ],
205 ), 264 ),
206 ], 265 ],
@@ -7,7 +7,6 @@ import 'package:get/get.dart'; @@ -7,7 +7,6 @@ import 'package:get/get.dart';
7 7
8 import '../controllers/user_onboarding_controller.dart'; 8 import '../controllers/user_onboarding_controller.dart';
9 import '../models/onboarding_option_data.dart'; 9 import '../models/onboarding_option_data.dart';
10 -import '../widget/guide_common_scaffold.dart';  
11 import '../widget/onboarding_common_widgets.dart'; 10 import '../widget/onboarding_common_widgets.dart';
12 import '../widget/onboarding_illustrations.dart'; 11 import '../widget/onboarding_illustrations.dart';
13 import '../widget/onboarding_em_rich_text.dart'; 12 import '../widget/onboarding_em_rich_text.dart';
  1 +import 'package:flutter/material.dart';
  2 +import 'widget.dart';
  3 +
  4 +///CircularGradientCombineWidget multi CircularGradientProgressWidget
  5 +class CircularGradientCombineWidget extends StatefulWidget {
  6 + ///widget size
  7 + final double size;
  8 +
  9 + ///list of each sweep angle
  10 + final List<double> sweepAngles;
  11 +
  12 + ///List<Color> of each circular
  13 + final List<List<Color>> gradientColors;
  14 +
  15 + ///Background Color of each circular
  16 + final List<Color> backgroundColors;
  17 +
  18 + ///empty center circle size ratio by size
  19 + final double centerCircleSizeRatio;
  20 +
  21 + ///each gap of circular ratio by size
  22 + final double gapRatio;
  23 +
  24 + /// default begin angle
  25 + final double initAngle;
  26 +
  27 + ///open animate
  28 + final bool animate;
  29 +
  30 + /// If true, a smoother intermediate color will be automatically inserted between the two gradient colors.
  31 + final bool interpolatedColor;
  32 +
  33 + /// Set the percentage of the gradient color based on the two colors when inserting the intermediate gradient color
  34 + final double interpolatedColorRatio;
  35 +
  36 + /// When the change reaches a certain percentage, the color will be the same as the last color,
  37 + //so it needs to be limited, otherwise a circular_gradient_progress with the same color will appear instead of a gradient color.
  38 + final double maxInterpolatedColorRatio;
  39 +
  40 + /// base duration
  41 + final Duration duration;
  42 +
  43 + /// This value represents how much animation time needs to be increased for each 360 degrees after exceeding 360 degrees.
  44 + final Duration plusDuration;
  45 +
  46 + /// max duration
  47 + final Duration maxDuration;
  48 +
  49 + /// animation behavier
  50 + final Curve curve;
  51 +
  52 + /// if true,be reverse
  53 + final bool reverse;
  54 +
  55 + ///
  56 + const CircularGradientCombineWidget({
  57 + super.key,
  58 + required this.size,
  59 + required this.sweepAngles,
  60 + required this.backgroundColors,
  61 + required this.gradientColors,
  62 + this.animate = true,
  63 + this.centerCircleSizeRatio = 0.15,
  64 + this.gapRatio = 0.05,
  65 + this.interpolatedColor = true,
  66 + this.interpolatedColorRatio = 0.1,
  67 + this.maxInterpolatedColorRatio = 0.7,
  68 + this.duration = const Duration(milliseconds: 1500),
  69 + this.plusDuration = const Duration(milliseconds: 500),
  70 + this.curve = Curves.easeInOutQuad,
  71 + this.initAngle = 0,
  72 + this.reverse = false,
  73 + this.maxDuration = Duration.zero,
  74 + });
  75 +
  76 + @override
  77 + State<CircularGradientCombineWidget> createState() => _CircularGradientCombineWidgetState();
  78 +}
  79 +
  80 +class _CircularGradientCombineWidgetState extends State<CircularGradientCombineWidget> with SingleTickerProviderStateMixin {
  81 + @override
  82 + Widget build(BuildContext context) {
  83 + final centerCircleSize = widget.size * widget.centerCircleSizeRatio;
  84 + final gap = widget.size * widget.gapRatio;
  85 + var strokeWidth = (widget.size - (widget.sweepAngles.length - 1) * gap - centerCircleSize) / widget.sweepAngles.length;
  86 + if (strokeWidth > widget.size / 3) {
  87 + strokeWidth = widget.size / 3;
  88 + }
  89 + return Stack(
  90 + alignment: Alignment.center,
  91 + children: List.generate(widget.sweepAngles.length, (index) {
  92 + return CircularGradientProgressWidget(
  93 + animate: widget.animate,
  94 + interpolatedColorRatio: widget.interpolatedColorRatio,
  95 + maxInterpolatedColorRatio: widget.maxInterpolatedColorRatio,
  96 + duration: widget.duration,
  97 + plusDuration: widget.plusDuration,
  98 + curve: widget.curve,
  99 + initAngle: widget.initAngle,
  100 + interpolatedColor: widget.interpolatedColor,
  101 + gradientColors: index >= widget.gradientColors.length ? widget.gradientColors.last : widget.gradientColors[index],
  102 + sweepAngle: index >= widget.sweepAngles.length ? widget.sweepAngles.last : widget.sweepAngles[index],
  103 + size: widget.size - strokeWidth * index - gap * index,
  104 + strokeWidth: strokeWidth / 2,
  105 + backgroundColor: index > widget.backgroundColors.length - 1 ? widget.backgroundColors.last : widget.backgroundColors[index],
  106 + reverse: widget.reverse,
  107 + maxDuration: widget.maxDuration,
  108 + );
  109 + }),
  110 + );
  111 + }
  112 +}
  1 +import 'dart:math' show cos, min, pi, sin;
  2 +
  3 +import 'package:flutter/material.dart';
  4 +
  5 +/// convert
  6 +extension NumToRadians on num {
  7 + /// toRadians
  8 + double toRadians() {
  9 + return toDouble() * (pi / 180.0);
  10 + }
  11 +
  12 + /// toDegree
  13 + double toDegree() {
  14 + return toDouble() * (180 / pi);
  15 + }
  16 +}
  17 +
  18 +/// painter for progress
  19 +class ProgressPainter extends CustomPainter {
  20 + ///0 to max
  21 + final double sweepAngle;
  22 +
  23 + /// circle stroke width
  24 + final double strokeWidth;
  25 +
  26 + /// multi color
  27 + final List<List<Color>> progressColor;
  28 +
  29 + /// base background color
  30 + final Color backgroundColor;
  31 +
  32 + /// reverse
  33 + final bool reverse;
  34 +
  35 + /// painter for progress
  36 + ProgressPainter({
  37 + this.sweepAngle = 360,
  38 + this.strokeWidth = 20,
  39 + this.reverse = false,
  40 + required this.progressColor,
  41 + required this.backgroundColor,
  42 + });
  43 +
  44 + @override
  45 + void paint(Canvas canvas, Size size) {
  46 + double wh = min(size.width, size.height);
  47 + var centerX = wh / 2;
  48 + var centerY = wh / 2;
  49 + var radius = wh / 2 - strokeWidth / 2;
  50 +
  51 + double startAngle = 0;
  52 + var rect = Rect.fromCenter(
  53 + center: Offset(centerX, centerY),
  54 + width: wh - strokeWidth,
  55 + height: wh - strokeWidth,
  56 + );
  57 + int colorRangeIndex = (sweepAngle / 360).ceil() - 1;
  58 + if (colorRangeIndex > progressColor.length - 1) {
  59 + colorRangeIndex = progressColor.length - 1;
  60 + }
  61 + if (colorRangeIndex < 0) {
  62 + colorRangeIndex = 0;
  63 + }
  64 +
  65 + var shader = SweepGradient(
  66 + startAngle: startAngle.toRadians(),
  67 + endAngle: 360.toRadians(),
  68 + colors: reverse ? progressColor[colorRangeIndex].reversed.toList() : progressColor[colorRangeIndex],
  69 + ).createShader(rect);
  70 +
  71 + var paint = Paint()
  72 + ..strokeWidth = strokeWidth
  73 + ..shader = shader
  74 + ..strokeCap = StrokeCap.butt
  75 + ..style = PaintingStyle.stroke;
  76 +
  77 + var backgroundPaint = Paint()
  78 + ..strokeWidth = strokeWidth
  79 + ..color = backgroundColor
  80 + ..strokeCap = StrokeCap.butt
  81 + ..style = PaintingStyle.stroke;
  82 +
  83 + var startCapPosition = Offset(
  84 + centerX + radius * cos(startAngle.toRadians()),
  85 + centerY + radius * sin(startAngle.toRadians()),
  86 + );
  87 +
  88 + canvas.drawArc(
  89 + rect,
  90 + 0.toRadians(),
  91 + 360.toRadians(),
  92 + false,
  93 + backgroundPaint,
  94 + );
  95 + if (sweepAngle >= 0) {
  96 + canvas.save();
  97 + // Move the canvas origin to the center
  98 + canvas.translate(centerX, centerY);
  99 + double rotationAngle = 0;
  100 +
  101 + if (sweepAngle > 360) {
  102 + rotationAngle = 360 - sweepAngle;
  103 + }
  104 + double rotationRadians = reverse ? -rotationAngle.toRadians() : rotationAngle.toRadians();
  105 + // Rotate the canvas
  106 + canvas.rotate((-90).toRadians() - rotationRadians);
  107 + // Move the canvas origin back
  108 + canvas.translate(-centerX, -centerY);
  109 +
  110 + if (sweepAngle < 360) {
  111 + canvas.drawArc(
  112 + Rect.fromCenter(
  113 + center: startCapPosition,
  114 + width: strokeWidth,
  115 + height: strokeWidth,
  116 + ),
  117 + 0,
  118 + reverse ? 180.toRadians() : -180.toRadians(),
  119 + true,
  120 + Paint()..color = progressColor[0].first,
  121 + );
  122 + }
  123 + canvas.drawArc(
  124 + rect,
  125 + 0.toRadians(),
  126 + reverse ? -(startAngle + sweepAngle).toRadians() : (startAngle + sweepAngle).toRadians(),
  127 + false,
  128 + paint,
  129 + );
  130 + if (sweepAngle < 360) {
  131 + canvas.drawRect(
  132 + Rect.fromCenter(
  133 + center: startCapPosition,
  134 + width: strokeWidth,
  135 + height: 1,
  136 + ),
  137 + Paint()..color = progressColor[0].first,
  138 + );
  139 + }
  140 +
  141 + canvas.translate(centerX, centerY);
  142 + if (sweepAngle < 360) {
  143 + final endCapRotate = 360 - sweepAngle;
  144 + canvas.rotate(reverse ? endCapRotate.toRadians() : -endCapRotate.toRadians());
  145 + }
  146 + //0.1 is because the angle calculation is not accurate enough, so we need to go back a little bit.
  147 + var endCapPosition = Offset(
  148 + centerX + radius * cos((startAngle).toRadians()),
  149 + centerY + radius * sin((startAngle).toRadians()),
  150 + );
  151 +
  152 + canvas.translate(-centerX, -centerY);
  153 + final endColor = getColorAtAngle(sweepAngle, progressColor[colorRangeIndex]);
  154 + if (sweepAngle > 355) {
  155 + //Draw arc shadow
  156 + var shadowPosition = Offset(
  157 + centerX + radius * cos((startAngle).toRadians()),
  158 + centerY + (reverse ? -5 : 5) + radius * sin((startAngle).toRadians()),
  159 + );
  160 + final Paint shadowPaint = Paint()
  161 + ..color = Colors.black.withOpacity(0.2)
  162 + ..style = PaintingStyle.stroke
  163 + ..strokeWidth = strokeWidth / 2
  164 + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3);
  165 + final Path path = Path()
  166 + ..addArc(
  167 + Rect.fromCenter(
  168 + center: shadowPosition,
  169 + width: strokeWidth / 2,
  170 + height: strokeWidth / 2,
  171 + ),
  172 + 0,
  173 + reverse ? -pi : pi,
  174 + );
  175 + canvas.drawPath(path, shadowPaint);
  176 + }
  177 +
  178 + //Draw the ending arc
  179 + canvas.drawArc(
  180 + Rect.fromCenter(
  181 + center: endCapPosition,
  182 + width: strokeWidth,
  183 + height: strokeWidth,
  184 + ),
  185 + 0,
  186 + reverse ? -180.toRadians() : 180.toRadians(),
  187 + true,
  188 + Paint()..color = endColor,
  189 + );
  190 + canvas.drawRect(
  191 + Rect.fromCenter(
  192 + center: endCapPosition,
  193 + width: strokeWidth,
  194 + height: 1,
  195 + ),
  196 + Paint()..color = endColor,
  197 + );
  198 +
  199 + // // Restore the canvas to the saved state
  200 + canvas.restore();
  201 + }
  202 + }
  203 +
  204 + @override
  205 + bool shouldRepaint(covariant ProgressPainter oldDelegate) {
  206 + return sweepAngle != oldDelegate.sweepAngle;
  207 + }
  208 +
  209 + /// get color by and angle
  210 + Color getColorAtAngle(double angle, List<Color> colors) {
  211 + double totalAngle = sweepAngle > 360 ? sweepAngle : 360;
  212 + double fraction = angle / totalAngle;
  213 + int segments = colors.length - 1;
  214 + double segmentFraction = fraction * segments;
  215 + int segmentIndex = segmentFraction.floor();
  216 + double segmentInnerFraction = segmentFraction - segmentIndex;
  217 + Color startColor = colors[segmentIndex];
  218 + Color endColor = colors[(segmentIndex + 1) % colors.length];
  219 + return Color.lerp(startColor, endColor, segmentInnerFraction)!;
  220 + }
  221 +}
  1 +import 'dart:math';
  2 +
  3 +import 'package:flutter/material.dart';
  4 +
  5 +import 'paint.dart';
  6 +
  7 +/// CircularGradientProgressWidget
  8 +class CircularGradientProgressWidget extends StatefulWidget {
  9 + ///widget size
  10 + final double size;
  11 +
  12 + ///circle size
  13 + final double strokeWidth;
  14 +
  15 + ///need > 0 ,< 0 means no data
  16 + final double sweepAngle;
  17 +
  18 + /// default begin angle
  19 + final double initAngle;
  20 +
  21 + ///The colors need to go from light to dark, otherwise the effect is weird.
  22 + ///Receive multiple color arrays so that more colors are needed when reaching a certain angle.
  23 + final List<Color> gradientColors;
  24 +
  25 + /// color of when angle is zero
  26 + final Color backgroundColor;
  27 +
  28 + ///open animate
  29 + final bool animate;
  30 +
  31 + /// If true, a smoother intermediate color will be automatically inserted between the two gradient colors.
  32 + final bool interpolatedColor;
  33 +
  34 + /// Set the percentage of the gradient color based on the two colors when inserting the intermediate gradient color
  35 + final double interpolatedColorRatio;
  36 +
  37 + /// When the change reaches a certain percentage, the color will be the same as the last color,
  38 + //so it needs to be limited, otherwise a circular_gradient_progress with the same color will appear instead of a gradient color.
  39 + final double maxInterpolatedColorRatio;
  40 +
  41 + /// base duration
  42 + final Duration duration;
  43 +
  44 + /// This value represents how much animation time needs to be increased for each 360 degrees after exceeding 360 degrees.
  45 + final Duration plusDuration;
  46 +
  47 + /// max duration
  48 + final Duration maxDuration;
  49 +
  50 + /// animation behavier
  51 + final Curve curve;
  52 +
  53 + /// if true,be reverse
  54 + final bool reverse;
  55 +
  56 + ///
  57 + const CircularGradientProgressWidget({
  58 + super.key,
  59 + required this.gradientColors,
  60 + required this.backgroundColor,
  61 + required this.size,
  62 + this.sweepAngle = 0,
  63 + required this.strokeWidth,
  64 + this.animate = false,
  65 + this.interpolatedColor = true,
  66 + this.interpolatedColorRatio = 0.1,
  67 + this.maxInterpolatedColorRatio = 0.7,
  68 + this.duration = const Duration(milliseconds: 1500),
  69 + this.plusDuration = const Duration(milliseconds: 500),
  70 + this.curve = Curves.easeInOutQuad,
  71 + this.initAngle = 0,
  72 + this.reverse = false,
  73 + this.maxDuration = Duration.zero,
  74 + });
  75 +
  76 + @override
  77 + State<CircularGradientProgressWidget> createState() => _CircularGradientProgressWidgetState();
  78 +}
  79 +
  80 +class _CircularGradientProgressWidgetState extends State<CircularGradientProgressWidget> with SingleTickerProviderStateMixin {
  81 + late AnimationController _controller;
  82 + late Animation<double> _animation;
  83 + double sweepAngle = 0;
  84 + double initAngle = 0;
  85 +
  86 + @override
  87 + void initState() {
  88 + super.initState();
  89 + initAngle = widget.initAngle;
  90 + if (widget.sweepAngle < 0) {
  91 + initAngle = -1;
  92 + }
  93 + _controller = AnimationController(
  94 + vsync: this,
  95 + duration: widget.duration,
  96 + );
  97 + _updateAnimation(widget.sweepAngle, initAngle);
  98 + }
  99 +
  100 + @override
  101 + void didUpdateWidget(CircularGradientProgressWidget oldWidget) {
  102 + super.didUpdateWidget(oldWidget);
  103 + if (widget.sweepAngle != oldWidget.sweepAngle) {
  104 + _updateAnimation(widget.sweepAngle, sweepAngle);
  105 + }
  106 + }
  107 +
  108 + void _updateAnimation(double newSweepAngle, double oldSweepAngle) {
  109 + if (widget.sweepAngle < 0 || newSweepAngle < 0) {
  110 + setState(() {
  111 + sweepAngle = -1;
  112 + });
  113 + return;
  114 + }
  115 + final double angleDifference = (newSweepAngle - oldSweepAngle).abs();
  116 + int baseDurationMillis = widget.duration.inMilliseconds;
  117 +
  118 + int totalDuration = 0;
  119 + if (angleDifference <= 360 && oldSweepAngle != 0) {
  120 + totalDuration = ((angleDifference / 360) * baseDurationMillis).toInt();
  121 + } else {
  122 + totalDuration = baseDurationMillis;
  123 + int full360Intervals = (angleDifference / 360).floor();
  124 + totalDuration += full360Intervals * widget.plusDuration.inMilliseconds;
  125 + }
  126 + if (widget.animate) {
  127 + if (widget.maxDuration.inMilliseconds != 0 && totalDuration > widget.maxDuration.inMilliseconds) {
  128 + _controller.duration = Duration(milliseconds: widget.maxDuration.inMilliseconds);
  129 + } else {
  130 + _controller.duration = Duration(milliseconds: totalDuration);
  131 + }
  132 + } else {
  133 + _controller.duration = Duration.zero;
  134 + }
  135 +
  136 + _animation = Tween<double>(begin: oldSweepAngle, end: newSweepAngle).animate(
  137 + CurvedAnimation(
  138 + parent: _controller,
  139 + curve: widget.curve,
  140 + ),
  141 + )..addListener(() {
  142 + setState(() {
  143 + sweepAngle = _animation.value;
  144 + });
  145 + });
  146 + _controller.forward(from: 0);
  147 + }
  148 +
  149 + @override
  150 + void dispose() {
  151 + _controller.dispose();
  152 + super.dispose();
  153 + }
  154 +
  155 + List<List<Color>> generateColorArray(List<Color> colorList, double totalAngle) {
  156 + final baseColorStart = colorList[0];
  157 + final baseColorEnd = colorList.length > 1 ? colorList[1] : colorList[0];
  158 + List<List<Color>> colors = [];
  159 + int fullRotations = (totalAngle / 360).floor();
  160 + colors.add([baseColorStart, baseColorEnd]);
  161 + for (int i = 1; i <= fullRotations; i++) {
  162 + double t = min(i * widget.interpolatedColorRatio, widget.maxInterpolatedColorRatio);
  163 + colors.add([Color.lerp(baseColorStart, baseColorEnd, t)!, baseColorEnd]);
  164 + }
  165 +
  166 + return colors;
  167 + }
  168 +
  169 + @override
  170 + Widget build(BuildContext context) {
  171 + final progressColors = generateColorArray(widget.gradientColors, widget.interpolatedColor ? sweepAngle : 0);
  172 + return CustomPaint(
  173 + painter: ProgressPainter(
  174 + sweepAngle: sweepAngle,
  175 + progressColor: progressColors,
  176 + strokeWidth: widget.strokeWidth,
  177 + backgroundColor: widget.backgroundColor,
  178 + reverse: widget.reverse,
  179 + ),
  180 + size: Size(
  181 + widget.size,
  182 + widget.size,
  183 + ),
  184 + );
  185 + }
  186 +}
@@ -61,9 +61,9 @@ @@ -61,9 +61,9 @@
61 "onboardingResearchEnergy": "元气满满", 61 "onboardingResearchEnergy": "元气满满",
62 "onboardingResearchHrvDown": "HRV下降", 62 "onboardingResearchHrvDown": "HRV下降",
63 "onboardingResearchHrvUp": "HRV上升", 63 "onboardingResearchHrvUp": "HRV上升",
64 - "onboardingHealthPermissionTitle": "允许访问健康数据", 64 + "onboardingHealthPermissionTitle": "开启健康授权提醒、统计你的压力",
65 "onboardingHealthPermissionBody": "DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。", 65 "onboardingHealthPermissionBody": "DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。",
66 - "onboardingHealthPermissionPrivacy": "请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。", 66 + "onboardingHealthPermissionPrivacy": "请放心,你的健康数据仅储存在本地,我们不上传任何相关数据。",
67 "onboardingNotificationTitle": "开启通知", 67 "onboardingNotificationTitle": "开启通知",
68 "onboardingNotificationSubtitle": "及时了解身体每一次异动", 68 "onboardingNotificationSubtitle": "及时了解身体每一次异动",
69 "onboardingNotificationBody": "AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态", 69 "onboardingNotificationBody": "AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态",
@@ -446,7 +446,7 @@ abstract class AppLocalizations { @@ -446,7 +446,7 @@ abstract class AppLocalizations {
446 /// No description provided for @onboardingHealthPermissionTitle. 446 /// No description provided for @onboardingHealthPermissionTitle.
447 /// 447 ///
448 /// In zh, this message translates to: 448 /// In zh, this message translates to:
449 - /// **'允许访问健康数据'** 449 + /// **'开启健康授权提醒、统计你的压力'**
450 String get onboardingHealthPermissionTitle; 450 String get onboardingHealthPermissionTitle;
451 451
452 /// No description provided for @onboardingHealthPermissionBody. 452 /// No description provided for @onboardingHealthPermissionBody.
@@ -458,7 +458,7 @@ abstract class AppLocalizations { @@ -458,7 +458,7 @@ abstract class AppLocalizations {
458 /// No description provided for @onboardingHealthPermissionPrivacy. 458 /// No description provided for @onboardingHealthPermissionPrivacy.
459 /// 459 ///
460 /// In zh, this message translates to: 460 /// In zh, this message translates to:
461 - /// **'请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。'** 461 + /// **'请放心,你的健康数据仅储存在本地,我们不上传任何相关数据。'**
462 String get onboardingHealthPermissionPrivacy; 462 String get onboardingHealthPermissionPrivacy;
463 463
464 /// No description provided for @onboardingNotificationTitle. 464 /// No description provided for @onboardingNotificationTitle.
  1 +// ignore: unused_import
  2 +import 'package:intl/intl.dart' as intl;
1 import 'app_localizations.dart'; 3 import 'app_localizations.dart';
2 4
3 // ignore_for_file: type=lint 5 // ignore_for_file: type=lint
@@ -70,7 +72,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -70,7 +72,8 @@ class AppLocalizationsZh extends AppLocalizations {
70 String get onboardingIntroTitle => 'DoubleFeel 是专为 Apple Watch 打造的健康陪伴app'; 72 String get onboardingIntroTitle => 'DoubleFeel 是专为 Apple Watch 打造的健康陪伴app';
71 73
72 @override 74 @override
73 - String get onboardingIntroBody => '我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>'; 75 + String get onboardingIntroBody =>
  76 + '我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>';
74 77
75 @override 78 @override
76 String get onboardingStateQuestion => '请问以下哪些描述,经常发生在你身上?'; 79 String get onboardingStateQuestion => '请问以下哪些描述,经常发生在你身上?';
@@ -160,7 +163,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -160,7 +163,8 @@ class AppLocalizationsZh extends AppLocalizations {
160 String get onboardingHrvSubtitle => '它能帮助我们衡量整体的压力和健康状态'; 163 String get onboardingHrvSubtitle => '它能帮助我们衡量整体的压力和健康状态';
161 164
162 @override 165 @override
163 - String get onboardingHrvDescription => '心率变异性(HRV, Heart Rate Variability)即心跳之间间隔时间的微小变化,反映了自主神经系统活动和身体对压力的反应能力'; 166 + String get onboardingHrvDescription =>
  167 + '心率变异性(HRV, Heart Rate Variability)即心跳之间间隔时间的微小变化,反映了自主神经系统活动和身体对压力的反应能力';
164 168
165 @override 169 @override
166 String get onboardingTellMeMore => '展开说说'; 170 String get onboardingTellMeMore => '展开说说';
@@ -181,13 +185,15 @@ class AppLocalizationsZh extends AppLocalizations { @@ -181,13 +185,15 @@ class AppLocalizationsZh extends AppLocalizations {
181 String get onboardingResearchHrvUp => 'HRV上升'; 185 String get onboardingResearchHrvUp => 'HRV上升';
182 186
183 @override 187 @override
184 - String get onboardingHealthPermissionTitle => '允许访问健康数据'; 188 + String get onboardingHealthPermissionTitle => '开启健康授权提醒、统计你的压力';
185 189
186 @override 190 @override
187 - String get onboardingHealthPermissionBody => 'DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。'; 191 + String get onboardingHealthPermissionBody =>
  192 + 'DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。';
188 193
189 @override 194 @override
190 - String get onboardingHealthPermissionPrivacy => '请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。'; 195 + String get onboardingHealthPermissionPrivacy =>
  196 + '请放心,你的健康数据仅储存在本地,我们不上传任何相关数据。';
191 197
192 @override 198 @override
193 String get onboardingNotificationTitle => '开启通知'; 199 String get onboardingNotificationTitle => '开启通知';
@@ -196,7 +202,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -196,7 +202,8 @@ class AppLocalizationsZh extends AppLocalizations {
196 String get onboardingNotificationSubtitle => '及时了解身体每一次异动'; 202 String get onboardingNotificationSubtitle => '及时了解身体每一次异动';
197 203
198 @override 204 @override
199 - String get onboardingNotificationBody => 'AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态'; 205 + String get onboardingNotificationBody =>
  206 + 'AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态';
200 207
201 @override 208 @override
202 String get onboardingMemberTitle => '获得年度会员优惠'; 209 String get onboardingMemberTitle => '获得年度会员优惠';
@@ -220,7 +227,9 @@ class AppLocalizationsZh extends AppLocalizations { @@ -220,7 +227,9 @@ class AppLocalizationsZh extends AppLocalizations {
220 String get healthCompanionIsNowAvailable => '健康陪伴已开启'; 227 String get healthCompanionIsNowAvailable => '健康陪伴已开启';
221 228
222 @override 229 @override
223 - String get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired => '你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'; 230 + String
  231 + get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired =>
  232 + '你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。';
224 233
225 @override 234 @override
226 String get bindPartnerTitle => '添加亲密联系人\n多一个人关注你的健康'; 235 String get bindPartnerTitle => '添加亲密联系人\n多一个人关注你的健康';
@@ -333,7 +342,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -333,7 +342,8 @@ class AppLocalizationsZh extends AppLocalizations {
333 String get todayHealthDataAuthTitle => '无法获取心率健康数据'; 342 String get todayHealthDataAuthTitle => '无法获取心率健康数据';
334 343
335 @override 344 @override
336 - String get todayHealthDataAuthDescription => 'DoubleFeel 需要授权访问你的健康数据,才能提供压力提醒、实时压力统计和健康建议;否则应用功能可能无法正常使用。请放心,你的健康数据仅存储在本地,不会上传到任何服务器。'; 345 + String get todayHealthDataAuthDescription =>
  346 + 'DoubleFeel 需要授权访问你的健康数据,才能提供压力提醒、实时压力统计和健康建议;否则应用功能可能无法正常使用。请放心,你的健康数据仅存储在本地,不会上传到任何服务器。';
337 347
338 @override 348 @override
339 String get todayHealthDataAuthAction => '授权访问健康数据'; 349 String get todayHealthDataAuthAction => '授权访问健康数据';
@@ -360,7 +370,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -360,7 +370,8 @@ class AppLocalizationsZh extends AppLocalizations {
360 String get todayFaqLinkWatchNoStatusNotification => '手表为什么无法收到状态通知?'; 370 String get todayFaqLinkWatchNoStatusNotification => '手表为什么无法收到状态通知?';
361 371
362 @override 372 @override
363 - String get todayFaqLinkWatchNoStatusAndInteractionNotification => '手表为什么无法收到状态和互动通知?'; 373 + String get todayFaqLinkWatchNoStatusAndInteractionNotification =>
  374 + '手表为什么无法收到状态和互动通知?';
364 375
365 @override 376 @override
366 String get todayFaqLinkWatchFaceDataDelay => '手表表盘数据不更新或者有延迟?'; 377 String get todayFaqLinkWatchFaceDataDelay => '手表表盘数据不更新或者有延迟?';
@@ -393,22 +404,27 @@ class AppLocalizationsZh extends AppLocalizations { @@ -393,22 +404,27 @@ class AppLocalizationsZh extends AppLocalizations {
393 String get todayStressStatusInsufficientData => '数据不足'; 404 String get todayStressStatusInsufficientData => '数据不足';
394 405
395 @override 406 @override
396 - String get todayStressStatusOverloadDescription => '当前 HRV 明显低于你的长期平均水平,可能意味着身体疲劳、压力过高或恢复不足。建议及时休息。'; 407 + String get todayStressStatusOverloadDescription =>
  408 + '当前 HRV 明显低于你的长期平均水平,可能意味着身体疲劳、压力过高或恢复不足。建议及时休息。';
397 409
398 @override 410 @override
399 - String get todayStressStatusCautionDescription => '当前 HRV 低于正常范围,身体可能正在积累压力,需要注意作息与恢复。'; 411 + String get todayStressStatusCautionDescription =>
  412 + '当前 HRV 低于正常范围,身体可能正在积累压力,需要注意作息与恢复。';
400 413
401 @override 414 @override
402 String get todayStressStatusNormalDescription => '当前身体状态处于你的正常波动范围内。'; 415 String get todayStressStatusNormalDescription => '当前身体状态处于你的正常波动范围内。';
403 416
404 @override 417 @override
405 - String get todayStressStatusExcellentDescription => '当前 HRV 高于近期平均水平,代表身体恢复与整体状态较好。'; 418 + String get todayStressStatusExcellentDescription =>
  419 + '当前 HRV 高于近期平均水平,代表身体恢复与整体状态较好。';
406 420
407 @override 421 @override
408 - String get todayStressStatusInsufficientDataDescription => '当前可用数据不足,暂时无法准确判断压力状态。'; 422 + String get todayStressStatusInsufficientDataDescription =>
  423 + '当前可用数据不足,暂时无法准确判断压力状态。';
409 424
410 @override 425 @override
411 - String get todayHrvMeasurementIntro => 'AppleWatch默认每2-5小时测量一次HRV,如果你希望立即手动进行测量,可以参考以下方法:'; 426 + String get todayHrvMeasurementIntro =>
  427 + 'AppleWatch默认每2-5小时测量一次HRV,如果你希望立即手动进行测量,可以参考以下方法:';
412 428
413 @override 429 @override
414 String get todayHrvMeasurementStep1 => '1、戴紧AppleWatch,坐下来,保持心境平和'; 430 String get todayHrvMeasurementStep1 => '1、戴紧AppleWatch,坐下来,保持心境平和';
@@ -426,7 +442,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -426,7 +442,8 @@ class AppLocalizationsZh extends AppLocalizations {
426 String get todayHrvMeasurementStep5 => '5、等待一分钟左右,StressWatch会收到你的数据并展示'; 442 String get todayHrvMeasurementStep5 => '5、等待一分钟左右,StressWatch会收到你的数据并展示';
427 443
428 @override 444 @override
429 - String get todayHrvMeasurementHint => '提示:数据源来自AppleWatch,在测量之后可能存在延迟或是数据未能同步的情况。如若出现上述情况,请重新测量并等待数据读取。'; 445 + String get todayHrvMeasurementHint =>
  446 + '提示:数据源来自AppleWatch,在测量之后可能存在延迟或是数据未能同步的情况。如若出现上述情况,请重新测量并等待数据读取。';
430 447
431 @override 448 @override
432 String get todayHrvMeasurementWarning => '注意:需打开健康里的权限,同时关闭省电模式。'; 449 String get todayHrvMeasurementWarning => '注意:需打开健康里的权限,同时关闭省电模式。';
@@ -435,10 +452,12 @@ class AppLocalizationsZh extends AppLocalizations { @@ -435,10 +452,12 @@ class AppLocalizationsZh extends AppLocalizations {
435 String get todayStressStatusWhatTitle => '什么是综合压力状态?'; 452 String get todayStressStatusWhatTitle => '什么是综合压力状态?';
436 453
437 @override 454 @override
438 - String get todayStressStatusWhatDescription1 => 'DoubleFeel 会结合你过去 30 天的 HRV(心率变异性)、静息心率以及当天的身体状态变化,综合评估你的整体压力水平。'; 455 + String get todayStressStatusWhatDescription1 =>
  456 + 'DoubleFeel 会结合你过去 30 天的 HRV(心率变异性)、静息心率以及当天的身体状态变化,综合评估你的整体压力水平。';
439 457
440 @override 458 @override
441 - String get todayStressStatusWhatDescription2 => '由于 HRV 会随着情绪、运动、睡眠和疲劳不断波动,单次数据参考意义有限,因此我们更建议关注一整天的综合压力状态,让结果更稳定、更有参考价值。综合压力不仅能帮助你了解自己的身体状态,也能让亲密联系人更及时地关注你的变化。'; 459 + String get todayStressStatusWhatDescription2 =>
  460 + '由于 HRV 会随着情绪、运动、睡眠和疲劳不断波动,单次数据参考意义有限,因此我们更建议关注一整天的综合压力状态,让结果更稳定、更有参考价值。综合压力不仅能帮助你了解自己的身体状态,也能让亲密联系人更及时地关注你的变化。';
442 461
443 @override 462 @override
444 String get todayStressStatusWhyHrvTitle => '为什么要参考 HRV(心率变异性)?'; 463 String get todayStressStatusWhyHrvTitle => '为什么要参考 HRV(心率变异性)?';
@@ -459,22 +478,27 @@ class AppLocalizationsZh extends AppLocalizations { @@ -459,22 +478,27 @@ class AppLocalizationsZh extends AppLocalizations {
459 String get todayStressStatusHrvChangesFast => '· HRV 变化较快,更适合观察短时间内的身体状态变化。'; 478 String get todayStressStatusHrvChangesFast => '· HRV 变化较快,更适合观察短时间内的身体状态变化。';
460 479
461 @override 480 @override
462 - String get todayStressStatusAppWatchDifferenceTitle => '手机 App 与 Apple Watch 显示的压力状态有什么区别?'; 481 + String get todayStressStatusAppWatchDifferenceTitle =>
  482 + '手机 App 与 Apple Watch 显示的压力状态有什么区别?';
463 483
464 @override 484 @override
465 - String get todayStressStatusAppWatchDifferenceApp => '手机 App 首页显示的是当天的综合压力状态,会综合分析 HRV、静息心率与整体趋势。'; 485 + String get todayStressStatusAppWatchDifferenceApp =>
  486 + '手机 App 首页显示的是当天的综合压力状态,会综合分析 HRV、静息心率与整体趋势。';
466 487
467 @override 488 @override
468 - String get todayStressStatusAppWatchDifferenceWatch => 'Apple Watch 显示的是最近一次的实时压力状态,更适合快速查看当前身体变化。'; 489 + String get todayStressStatusAppWatchDifferenceWatch =>
  490 + 'Apple Watch 显示的是最近一次的实时压力状态,更适合快速查看当前身体变化。';
469 491
470 @override 492 @override
471 String get todayStressStatusWaitingDataTitle => '为什么会出现“等待数据”?'; 493 String get todayStressStatusWaitingDataTitle => '为什么会出现“等待数据”?';
472 494
473 @override 495 @override
474 - String get todayStressStatusWaitingDataDescription1 => '“等待数据”代表当前采集到的数据量不足,暂时无法生成可靠的压力评估。'; 496 + String get todayStressStatusWaitingDataDescription1 =>
  497 + '“等待数据”代表当前采集到的数据量不足,暂时无法生成可靠的压力评估。';
475 498
476 @override 499 @override
477 - String get todayStressStatusWaitingDataDescription2 => '请继续佩戴 Apple Watch,等待系统自动采集数据。'; 500 + String get todayStressStatusWaitingDataDescription2 =>
  501 + '请继续佩戴 Apple Watch,等待系统自动采集数据。';
478 502
479 @override 503 @override
480 String get todayStressStatusWaitingDataReasonsIntro => '可能原因包括:'; 504 String get todayStressStatusWaitingDataReasonsIntro => '可能原因包括:';
@@ -495,28 +519,35 @@ class AppLocalizationsZh extends AppLocalizations { @@ -495,28 +519,35 @@ class AppLocalizationsZh extends AppLocalizations {
495 String get todayHrvPrincipleHowMeasureTitle => 'DoubleFeel 如何测量压力状态?'; 519 String get todayHrvPrincipleHowMeasureTitle => 'DoubleFeel 如何测量压力状态?';
496 520
497 @override 521 @override
498 - String get todayHrvPrincipleHowMeasureDescription1 => '当你正常佩戴 Apple Watch 时,系统会自动采集你的心率数据,并同步至 Apple Health。'; 522 + String get todayHrvPrincipleHowMeasureDescription1 =>
  523 + '当你正常佩戴 Apple Watch 时,系统会自动采集你的心率数据,并同步至 Apple Health。';
499 524
500 @override 525 @override
501 - String get todayHrvPrincipleHowMeasureDescription2 => 'DoubleFeel 会基于这些数据计算 HRV(心率变异性)相关指标,用于评估你的身体压力与恢复状态。'; 526 + String get todayHrvPrincipleHowMeasureDescription2 =>
  527 + 'DoubleFeel 会基于这些数据计算 HRV(心率变异性)相关指标,用于评估你的身体压力与恢复状态。';
502 528
503 @override 529 @override
504 - String get todayHrvPrincipleHowMeasureDescription3 => 'HRV 对压力、疲劳、睡眠、情绪与身体恢复都非常敏感,因此它能够帮助我们更早发现身体状态变化。'; 530 + String get todayHrvPrincipleHowMeasureDescription3 =>
  531 + 'HRV 对压力、疲劳、睡眠、情绪与身体恢复都非常敏感,因此它能够帮助我们更早发现身体状态变化。';
505 532
506 @override 533 @override
507 - String get todayHrvPrincipleHowMeasureDescription4 => '为了让结果更准确,DoubleFeel 会将你当前的 HRV 状态与过去 30 天的个人平均水平进行对比,而不是直接与其他人比较。'; 534 + String get todayHrvPrincipleHowMeasureDescription4 =>
  535 + '为了让结果更准确,DoubleFeel 会将你当前的 HRV 状态与过去 30 天的个人平均水平进行对比,而不是直接与其他人比较。';
508 536
509 @override 537 @override
510 String get todayRealtimeStressWhatTitle => '什么是实时压力?'; 538 String get todayRealtimeStressWhatTitle => '什么是实时压力?';
511 539
512 @override 540 @override
513 - String get todayRealtimeStressWhatDescription1 => '实时压力是 DoubleFeel 根据你当前的 HRV、心率状态与个人历史数据变化,动态生成的身体压力指标。'; 541 + String get todayRealtimeStressWhatDescription1 =>
  542 + '实时压力是 DoubleFeel 根据你当前的 HRV、心率状态与个人历史数据变化,动态生成的身体压力指标。';
514 543
515 @override 544 @override
516 - String get todayRealtimeStressWhatDescription2 => '压力值越高,代表你的身体状态相比平时偏离越明显,可能正处于疲劳、恢复不足或高压力状态。'; 545 + String get todayRealtimeStressWhatDescription2 =>
  546 + '压力值越高,代表你的身体状态相比平时偏离越明显,可能正处于疲劳、恢复不足或高压力状态。';
517 547
518 @override 548 @override
519 - String get todayRealtimeStressWhatDescription3 => '它能够帮助你更快发现身体变化,并及时调整休息、运动与生活节奏。'; 549 + String get todayRealtimeStressWhatDescription3 =>
  550 + '它能够帮助你更快发现身体变化,并及时调整休息、运动与生活节奏。';
520 551
521 @override 552 @override
522 String get todayRealtimeStressDivisionTitle => '实时压力如何划分?'; 553 String get todayRealtimeStressDivisionTitle => '实时压力如何划分?';
@@ -546,10 +577,12 @@ class AppLocalizationsZh extends AppLocalizations { @@ -546,10 +577,12 @@ class AppLocalizationsZh extends AppLocalizations {
546 String get todayRealtimeStressCautionDescription => '身体可能正在积累压力,需要适当休息与恢复。'; 577 String get todayRealtimeStressCautionDescription => '身体可能正在积累压力,需要适当休息与恢复。';
547 578
548 @override 579 @override
549 - String get todayRealtimeStressOverloadDescription => '身体压力明显偏高,建议减少负荷、注意睡眠与恢复。'; 580 + String get todayRealtimeStressOverloadDescription =>
  581 + '身体压力明显偏高,建议减少负荷、注意睡眠与恢复。';
550 582
551 @override 583 @override
552 - String get todayRealtimeStressDivisionBaseline => '以上区间会结合你的个人基线动态调整,不同用户之间并不直接比较。'; 584 + String get todayRealtimeStressDivisionBaseline =>
  585 + '以上区间会结合你的个人基线动态调整,不同用户之间并不直接比较。';
553 586
554 @override 587 @override
555 String get todayRealtimeStressDivisionAwake => '此外,实时压力主要反映清醒状态下的身体压力变化。'; 588 String get todayRealtimeStressDivisionAwake => '此外,实时压力主要反映清醒状态下的身体压力变化。';
@@ -564,25 +597,31 @@ class AppLocalizationsZh extends AppLocalizations { @@ -564,25 +597,31 @@ class AppLocalizationsZh extends AppLocalizations {
564 String get todayRealtimeStressLowBetterType => '身体压力分为“正常压力”与“异常压力”。'; 597 String get todayRealtimeStressLowBetterType => '身体压力分为“正常压力”与“异常压力”。';
565 598
566 @override 599 @override
567 - String get todayRealtimeStressLowBetterExample => '例如:运动期间或运动后,实时压力短时间升高属于正常恢复反应;工作专注、情绪兴奋时,压力也可能暂时升高,这些都属于正常的身体调节。'; 600 + String get todayRealtimeStressLowBetterExample =>
  601 + '例如:运动期间或运动后,实时压力短时间升高属于正常恢复反应;工作专注、情绪兴奋时,压力也可能暂时升高,这些都属于正常的身体调节。';
568 602
569 @override 603 @override
570 - String get todayRealtimeStressLowBetterHighStress => '但如果在静息、久坐或睡眠不足的情况下,压力长期偏高,则可能意味着身体疲劳、心理压力较大、睡眠恢复不足、运动恢复不充分、摄入过多咖啡因、酒精或刺激物、身体可能处于不适状态。'; 604 + String get todayRealtimeStressLowBetterHighStress =>
  605 + '但如果在静息、久坐或睡眠不足的情况下,压力长期偏高,则可能意味着身体疲劳、心理压力较大、睡眠恢复不足、运动恢复不充分、摄入过多咖啡因、酒精或刺激物、身体可能处于不适状态。';
571 606
572 @override 607 @override
573 - String get todayRealtimeStressLowBetterTrend => 'DoubleFeel 更关注的是你的长期变化趋势,而不是单次波动。'; 608 + String get todayRealtimeStressLowBetterTrend =>
  609 + 'DoubleFeel 更关注的是你的长期变化趋势,而不是单次波动。';
574 610
575 @override 611 @override
576 String get todayRealtimeStressScenarioTitle => 'HRV 与实时压力适用场景?'; 612 String get todayRealtimeStressScenarioTitle => 'HRV 与实时压力适用场景?';
577 613
578 @override 614 @override
579 - String get todayRealtimeStressScenarioHrvDefault => '在 Apple Watch 的默认设置下,HRV 每 2~5 小时更新一次。'; 615 + String get todayRealtimeStressScenarioHrvDefault =>
  616 + '在 Apple Watch 的默认设置下,HRV 每 2~5 小时更新一次。';
580 617
581 @override 618 @override
582 - String get todayRealtimeStressScenarioRegionLimit => '在部分地区,由于 Apple Watch 的呼吸功能受限,HRV 的更新频率可能会受到影响,并且开启呼吸功能后也会消耗更多电量。'; 619 + String get todayRealtimeStressScenarioRegionLimit =>
  620 + '在部分地区,由于 Apple Watch 的呼吸功能受限,HRV 的更新频率可能会受到影响,并且开启呼吸功能后也会消耗更多电量。';
583 621
584 @override 622 @override
585 - String get todayRealtimeStressScenarioIntro => '为了解决 HRV 更新间隔较长的问题,DoubleFeel 设计了实时压力功能:'; 623 + String get todayRealtimeStressScenarioIntro =>
  624 + '为了解决 HRV 更新间隔较长的问题,DoubleFeel 设计了实时压力功能:';
586 625
587 @override 626 @override
588 String get todayRealtimeStressScenarioUpdateEvery6Min => '· 实时压力每 6 分钟更新一次'; 627 String get todayRealtimeStressScenarioUpdateEvery6Min => '· 实时压力每 6 分钟更新一次';
@@ -591,22 +630,27 @@ class AppLocalizationsZh extends AppLocalizations { @@ -591,22 +630,27 @@ class AppLocalizationsZh extends AppLocalizations {
591 String get todayRealtimeStressScenarioTimely => '· 可以更及时地反映身体状态变化'; 630 String get todayRealtimeStressScenarioTimely => '· 可以更及时地反映身体状态变化';
592 631
593 @override 632 @override
594 - String get todayRealtimeStressScenarioConsistentTrend => '· 在大多数情况下,实时压力趋势与 HRV 趋势是一致的'; 633 + String get todayRealtimeStressScenarioConsistentTrend =>
  634 + '· 在大多数情况下,实时压力趋势与 HRV 趋势是一致的';
595 635
596 @override 636 @override
597 - String get todayRealtimeStressScenarioSummary => '这样用户既能获得 HRV 的长期趋势,也能通过实时压力获得短时身体状态的参考。'; 637 + String get todayRealtimeStressScenarioSummary =>
  638 + '这样用户既能获得 HRV 的长期趋势,也能通过实时压力获得短时身体状态的参考。';
598 639
599 @override 640 @override
600 String get todayFaqNoDataTitle => 'APP或表盘有没有数据怎么办?'; 641 String get todayFaqNoDataTitle => 'APP或表盘有没有数据怎么办?';
601 642
602 @override 643 @override
603 - String get todayFaqNoDataDescription1 => '1. 确认苹果手表系统在10.0以上,手机系统在14以上,系统版本可在「关于本机」内查看。'; 644 + String get todayFaqNoDataDescription1 =>
  645 + '1. 确认苹果手表系统在10.0以上,手机系统在14以上,系统版本可在「关于本机」内查看。';
604 646
605 @override 647 @override
606 - String get todayFaqNoDataDescription2 => '2. 确认是否开启所有权限:手机「健康」-「共享」-「app」-「DoubleFeel」-「打开所有权限」。'; 648 + String get todayFaqNoDataDescription2 =>
  649 + '2. 确认是否开启所有权限:手机「健康」-「共享」-「app」-「DoubleFeel」-「打开所有权限」。';
607 650
608 @override 651 @override
609 - String get todayFaqNoDataDescription3 => '3. 确认设备是否处于省电模式、低电量状态或手表佩戴未贴紧,以上情况会影响手表数据采集。'; 652 + String get todayFaqNoDataDescription3 =>
  653 + '3. 确认设备是否处于省电模式、低电量状态或手表佩戴未贴紧,以上情况会影响手表数据采集。';
610 654
611 @override 655 @override
612 String get todayFaqContactPrefix => '如以上均检查无问题,可以'; 656 String get todayFaqContactPrefix => '如以上均检查无问题,可以';
@@ -621,37 +665,46 @@ class AppLocalizationsZh extends AppLocalizations { @@ -621,37 +665,46 @@ class AppLocalizationsZh extends AppLocalizations {
621 String get todayFaqWatchNoNotificationTitle => '手表无法收到状态通知?'; 665 String get todayFaqWatchNoNotificationTitle => '手表无法收到状态通知?';
622 666
623 @override 667 @override
624 - String get todayFaqWatchNoNotificationDescription1 => '苹果手表和手机的通知展示有优先级:当手机已解锁并亮屏时,通知只会在手机端展示,不会在手表上出现。'; 668 + String get todayFaqWatchNoNotificationDescription1 =>
  669 + '苹果手表和手机的通知展示有优先级:当手机已解锁并亮屏时,通知只会在手机端展示,不会在手表上出现。';
625 670
626 @override 671 @override
627 - String get todayFaqWatchNoNotificationDescription2 => '若压力数据可正常显示和自动更新,但手表未收到通知,可尝试以下操作:'; 672 + String get todayFaqWatchNoNotificationDescription2 =>
  673 + '若压力数据可正常显示和自动更新,但手表未收到通知,可尝试以下操作:';
628 674
629 @override 675 @override
630 - String get todayFaqWatchNoNotificationCheckPhoneNotification => '1. 检查手机是否打开通知(设置-DoubleFeel-通知)。'; 676 + String get todayFaqWatchNoNotificationCheckPhoneNotification =>
  677 + '1. 检查手机是否打开通知(设置-DoubleFeel-通知)。';
631 678
632 @override 679 @override
633 - String get todayFaqWatchNoNotificationCheckPhoneBackgroundRefresh => '2. 检查手机是否打开后台App刷新(设置-DoubleFeel-后台App刷新)。'; 680 + String get todayFaqWatchNoNotificationCheckPhoneBackgroundRefresh =>
  681 + '2. 检查手机是否打开后台App刷新(设置-DoubleFeel-后台App刷新)。';
634 682
635 @override 683 @override
636 - String get todayFaqWatchNoNotificationCheckWatchBackgroundRefresh => '3. 检查手表是否打开后台App刷新(设置-通用-后台App刷新,并确保DoubleFeel开启)。'; 684 + String get todayFaqWatchNoNotificationCheckWatchBackgroundRefresh =>
  685 + '3. 检查手表是否打开后台App刷新(设置-通用-后台App刷新,并确保DoubleFeel开启)。';
637 686
638 @override 687 @override
639 - String get todayFaqWatchNoNotificationCheckModes => '4. 确保未处于低电量/专注/勿扰/剧院/睡眠等模式。'; 688 + String get todayFaqWatchNoNotificationCheckModes =>
  689 + '4. 确保未处于低电量/专注/勿扰/剧院/睡眠等模式。';
640 690
641 @override 691 @override
642 - String get todayFaqWatchNoNotificationReinstall => '5. 重装DoubleFeel 并重启AppleWatch与iPhone。'; 692 + String get todayFaqWatchNoNotificationReinstall =>
  693 + '5. 重装DoubleFeel 并重启AppleWatch与iPhone。';
643 694
644 @override 695 @override
645 String get todayFaqWatchFaceDelayTitle => '手表表盘数据不更新或有延迟?'; 696 String get todayFaqWatchFaceDelayTitle => '手表表盘数据不更新或有延迟?';
646 697
647 @override 698 @override
648 - String get todayFaqWatchFaceDelayDescription1 => '由于苹果系统限制,所有手表表盘(第三方或官方)都会存在几分钟至半小时的延迟,开发者无法控制刷新频率。'; 699 + String get todayFaqWatchFaceDelayDescription1 =>
  700 + '由于苹果系统限制,所有手表表盘(第三方或官方)都会存在几分钟至半小时的延迟,开发者无法控制刷新频率。';
649 701
650 @override 702 @override
651 String get todayFaqWatchFaceDelayIfOverOneHour => '若手机数据刷新后超过1小时表盘仍未更新:'; 703 String get todayFaqWatchFaceDelayIfOverOneHour => '若手机数据刷新后超过1小时表盘仍未更新:';
652 704
653 @override 705 @override
654 - String get todayFaqWatchFaceDelayOpenWatchApp => '请在手表上手动打开DoubleFeel,等待约1分钟。'; 706 + String get todayFaqWatchFaceDelayOpenWatchApp =>
  707 + '请在手表上手动打开DoubleFeel,等待约1分钟。';
655 708
656 @override 709 @override
657 String get todayFaqWatchFaceDelayIfStill => '若仍未更新:'; 710 String get todayFaqWatchFaceDelayIfStill => '若仍未更新:';
@@ -666,22 +719,27 @@ class AppLocalizationsZh extends AppLocalizations { @@ -666,22 +719,27 @@ class AppLocalizationsZh extends AppLocalizations {
666 String get todayFaqWatchFaceDelayCheckData => '· 手机和手表app是否可正常看到HRV数据。'; 719 String get todayFaqWatchFaceDelayCheckData => '· 手机和手表app是否可正常看到HRV数据。';
667 720
668 @override 721 @override
669 - String get todayFaqWatchFaceDelayCheckPhoneHealth => '· 确保手机端「iOS设置-隐私与安全性-健康-DoubleFeel」全部授权。'; 722 + String get todayFaqWatchFaceDelayCheckPhoneHealth =>
  723 + '· 确保手机端「iOS设置-隐私与安全性-健康-DoubleFeel」全部授权。';
670 724
671 @override 725 @override
672 - String get todayFaqWatchFaceDelayCheckWatchHealth => '· 确保手表端「设置-健康-数据来源、App和服务-DoubleFeel」全部授权。'; 726 + String get todayFaqWatchFaceDelayCheckWatchHealth =>
  727 + '· 确保手表端「设置-健康-数据来源、App和服务-DoubleFeel」全部授权。';
673 728
674 @override 729 @override
675 - String get todayFaqWatchFaceDelayCheckBackgroundRefresh => '· 确认AppleWatch-设置-通用-后台App刷新中DoubleFeel已开启。'; 730 + String get todayFaqWatchFaceDelayCheckBackgroundRefresh =>
  731 + '· 确认AppleWatch-设置-通用-后台App刷新中DoubleFeel已开启。';
676 732
677 @override 733 @override
678 - String get todayFaqWatchFaceDelayRestartWatch => '· 若仍未自动刷新,请重启手表。长时间运行或后台占用过高可能导致表盘暂停更新。'; 734 + String get todayFaqWatchFaceDelayRestartWatch =>
  735 + '· 若仍未自动刷新,请重启手表。长时间运行或后台占用过高可能导致表盘暂停更新。';
679 736
680 @override 737 @override
681 String get todayFaqWatchFaceBlackScreenTitle => '手表表盘出现黑屏?'; 738 String get todayFaqWatchFaceBlackScreenTitle => '手表表盘出现黑屏?';
682 739
683 @override 740 @override
684 - String get todayFaqWatchFaceBlackScreenDescription => '若添加专属互动表盘后出现黑屏(仅显示时间和日期),可长按表盘,点击「编辑」,左滑至「复杂功能」,选择 DoubleFeel,然后按需选择各组件重新添加。'; 741 + String get todayFaqWatchFaceBlackScreenDescription =>
  742 + '若添加专属互动表盘后出现黑屏(仅显示时间和日期),可长按表盘,点击「编辑」,左滑至「复杂功能」,选择 DoubleFeel,然后按需选择各组件重新添加。';
685 743
686 @override 744 @override
687 String get today => '今天'; 745 String get today => '今天';
@@ -765,7 +823,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -765,7 +823,8 @@ class AppLocalizationsZh extends AppLocalizations {
765 String get questionsAndFeedback => '问题和反馈'; 823 String get questionsAndFeedback => '问题和反馈';
766 824
767 @override 825 @override
768 - String get ifYouWouldLikeUsToReplyPleaseProvideYourEmailAddress => '如果需要我们回复,请填写联系邮箱'; 826 + String get ifYouWouldLikeUsToReplyPleaseProvideYourEmailAddress =>
  827 + '如果需要我们回复,请填写联系邮箱';
769 828
770 @override 829 @override
771 String get uploadProof => '上传凭证'; 830 String get uploadProof => '上传凭证';
  1 +import 'dart:async';
  2 +
  3 +import 'package:flutter/foundation.dart';
1 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
2 import 'package:flutter/services.dart'; 5 import 'package:flutter/services.dart';
3 import 'package:intl/date_symbol_data_local.dart'; 6 import 'package:intl/date_symbol_data_local.dart';
@@ -7,11 +10,22 @@ import 'app/double_feel_app.dart'; @@ -7,11 +10,22 @@ import 'app/double_feel_app.dart';
7 import 'core/theme/app_theme.dart'; 10 import 'core/theme/app_theme.dart';
8 11
9 Future<void> main() async { 12 Future<void> main() async {
10 - WidgetsFlutterBinding.ensureInitialized(); 13 + runZonedGuarded<Future<Null>>(
  14 + () async {
  15 + WidgetsFlutterBinding.ensureInitialized();
  16 + FlutterError.onError = (errorDetails) {};
  17 + PlatformDispatcher.instance.onError = (error, stack) {
  18 + return true;
  19 + };
11 20
12 - SystemChrome.setSystemUIOverlayStyle(AppTheme.systemUiOverlayStyle); 21 + SystemChrome.setSystemUIOverlayStyle(AppTheme.systemUiOverlayStyle);
13 22
14 - await AppBootstrap.init();  
15 - await initializeDateFormatting();  
16 - runApp(const DoubleFeelApp()); 23 + await AppBootstrap.init();
  24 + await initializeDateFormatting();
  25 + runApp(const DoubleFeelApp());
  26 + },
  27 + (error, stackTrace) async {
  28 + if (kDebugMode) {}
  29 + },
  30 + );
17 } 31 }
@@ -70,5 +70,5 @@ flutter: @@ -70,5 +70,5 @@ flutter:
70 - assets/images/today/ 70 - assets/images/today/
71 - assets/images/friends/ 71 - assets/images/friends/
72 - assets/images/my/ 72 - assets/images/my/
73 - - assets/images/my/ 73 + - assets/images/permission/
74 - assets/images/watch_theme/ 74 - assets/images/watch_theme/