Commit 068524a795c0582b497d3a75a08dc626795edcfe

Authored by 常守达
1 parent 7b773e28

fix(ui):验收修复

@@ -97,13 +97,12 @@ class HomeController extends GetxController { @@ -97,13 +97,12 @@ class HomeController extends GetxController {
97 97
98 final result = await Get.find<ClickEventApi>().getClickEvent(); 98 final result = await Get.find<ClickEventApi>().getClickEvent();
99 return switch (result) { 99 return switch (result) {
100 - AppSuccess(:final data) =>  
101 - data.list?.any(  
102 - (item) =>  
103 - EventConst.keys.contains(item.eventName) &&  
104 - (item.clickCnt ?? 0) > 0,  
105 - ) ??  
106 - false, 100 + AppSuccess(:final data) => data.list?.any(
  101 + (item) =>
  102 + EventConst.keys.contains(item.eventName) &&
  103 + (item.clickCnt ?? 0) > 0,
  104 + ) ??
  105 + false,
107 AppFailure() => false, 106 AppFailure() => false,
108 }; 107 };
109 } 108 }
@@ -156,8 +155,12 @@ class HomeController extends GetxController { @@ -156,8 +155,12 @@ class HomeController extends GetxController {
156 } 155 }
157 } 156 }
158 157
159 - void openTrend(TrendType type, {DateTime? date, ReportPeriod? period}) { 158 + void openTrend(TrendType type,
  159 + {DateTime? date, ReportPeriod? period, bool fromTodayTab = true}) {
160 Get.find<TrendController>().selectType(type, date: date, period: period); 160 Get.find<TrendController>().selectType(type, date: date, period: period);
  161 + if (fromTodayTab) {
  162 + Get.find<TrendController>().selectSelf();
  163 + }
161 changeTab(trendTabIndex); 164 changeTab(trendTabIndex);
162 } 165 }
163 166
@@ -268,7 +271,7 @@ class HomeController extends GetxController { @@ -268,7 +271,7 @@ class HomeController extends GetxController {
268 final type = params['type']; 271 final type = params['type'];
269 final trendType = type == null ? null : getTrendType(type); 272 final trendType = type == null ? null : getTrendType(type);
270 if (trendType != null) { 273 if (trendType != null) {
271 - openTrend(trendType); 274 + openTrend(trendType, fromTodayTab: false);
272 } else { 275 } else {
273 changeTab(trendTabIndex); 276 changeTab(trendTabIndex);
274 } 277 }
@@ -132,28 +132,9 @@ class TodayTabBody extends StatelessWidget { @@ -132,28 +132,9 @@ class TodayTabBody extends StatelessWidget {
132 slivers: [ 132 slivers: [
133 SliverToBoxAdapter( 133 SliverToBoxAdapter(
134 child: Container( 134 child: Container(
135 - height: 323 + pinnedHeaderHeight, 135 + width: Get.width,
  136 + height: Get.width / 375 * (323 + pinnedHeaderHeight),
136 padding: EdgeInsets.only(bottom: 32), 137 padding: EdgeInsets.only(bottom: 32),
137 - // decoration: const BoxDecoration(  
138 - // gradient: LinearGradient(  
139 - // begin: Alignment.topCenter,  
140 - // end: Alignment.bottomCenter,  
141 - // colors: [  
142 - // Color(0xFFC5B0FF),  
143 - // Color(0xFFF5F2FF),  
144 - // ],  
145 - // ),  
146 - // ),  
147 - // decoration: BoxDecoration(  
148 - // image: DecorationImage(  
149 - // image: AssetImage(  
150 - // controller.v2StressScore.value?.stateBg() ??  
151 - // 'assets/images/today/bg_stress_state_0.png',  
152 - // ),  
153 - // fit: BoxFit.cover,  
154 - // alignment: Alignment.topCenter,  
155 - // ),  
156 - // ),  
157 child: Container( 138 child: Container(
158 // margin: EdgeInsets.only(top: pinnedHeaderHeight), 139 // margin: EdgeInsets.only(top: pinnedHeaderHeight),
159 child: Column( 140 child: Column(
@@ -164,7 +145,8 @@ class TodayTabBody extends StatelessWidget { @@ -164,7 +145,8 @@ class TodayTabBody extends StatelessWidget {
164 controller.v2StressScore.value?.stateBg() ?? 145 controller.v2StressScore.value?.stateBg() ??
165 'assets/images/today/bg_stress_state_0.png', 146 'assets/images/today/bg_stress_state_0.png',
166 fit: BoxFit.cover, 147 fit: BoxFit.cover,
167 - width: double.infinity, 148 + width: Get.width,
  149 + height: Get.width / 375 * 345,
168 )), 150 )),
169 ), 151 ),
170 Obx( 152 Obx(
@@ -62,13 +62,11 @@ class HrvMeasurementBottomSheet extends StatelessWidget { @@ -62,13 +62,11 @@ class HrvMeasurementBottomSheet extends StatelessWidget {
62 ), 62 ),
63 ), 63 ),
64 Expanded( 64 Expanded(
65 - child: Padding(  
66 - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), 65 + child: Container(
  66 + margin: EdgeInsets.only(left: 16, right: 16),
  67 + padding: const EdgeInsets.only(top: 16, bottom: 32),
67 child: Center( 68 child: Center(
68 - child: ConstrainedBox(  
69 - constraints: const BoxConstraints(maxWidth: 342),  
70 - child: const _HrvInstructionCard(),  
71 - ), 69 + child: const _HrvInstructionCard(),
72 ), 70 ),
73 ), 71 ),
74 ), 72 ),
@@ -287,9 +287,9 @@ class _FaqLink extends StatelessWidget { @@ -287,9 +287,9 @@ class _FaqLink extends StatelessWidget {
287 style: TextStyle( 287 style: TextStyle(
288 color: context.colors.textSecondary, 288 color: context.colors.textSecondary,
289 fontSize: 12, 289 fontSize: 12,
290 - height: 17 / 12,  
291 fontWeight: FontWeight.w400, 290 fontWeight: FontWeight.w400,
292 decoration: TextDecoration.underline, 291 decoration: TextDecoration.underline,
  292 + decorationColor: context.colors.textSecondary,
293 ), 293 ),
294 ), 294 ),
295 ), 295 ),
@@ -322,6 +322,7 @@ class _FaqLink extends StatelessWidget { @@ -322,6 +322,7 @@ class _FaqLink extends StatelessWidget {
322 height: 17 / 12, 322 height: 17 / 12,
323 fontWeight: FontWeight.w400, 323 fontWeight: FontWeight.w400,
324 decoration: TextDecoration.underline, 324 decoration: TextDecoration.underline,
  325 + decorationColor: context.colors.textSecondary,
325 ), 326 ),
326 ), 327 ),
327 ), 328 ),
@@ -323,6 +323,7 @@ class _FaqLink extends StatelessWidget { @@ -323,6 +323,7 @@ class _FaqLink extends StatelessWidget {
323 height: 17 / 12, 323 height: 17 / 12,
324 fontWeight: FontWeight.w400, 324 fontWeight: FontWeight.w400,
325 decoration: TextDecoration.underline, 325 decoration: TextDecoration.underline,
  326 + decorationColor: context.colors.textSecondary,
326 ), 327 ),
327 ), 328 ),
328 ), 329 ),
@@ -60,8 +60,18 @@ class TodayHrvChartCard extends StatelessWidget { @@ -60,8 +60,18 @@ class TodayHrvChartCard extends StatelessWidget {
60 const SizedBox(height: 14), 60 const SizedBox(height: 14),
61 SizedBox( 61 SizedBox(
62 height: 174, 62 height: 174,
63 - child: controller.isLoadingToday.value  
64 - ? Center( 63 + child: Stack(
  64 + children: [
  65 + _HrvLineChart(
  66 + spots: hrvSpots,
  67 + stateMap: hrvStateMap,
  68 + maxSeconds: _maxChartSeconds(),
  69 + getColorByState: _getColorByState,
  70 + getTooltipChildren: _getTooltipChildren,
  71 + timeLabel: _timeLabel,
  72 + ),
  73 + if (controller.isLoadingToday.value)
  74 + Center(
65 child: Lottie.asset( 75 child: Lottie.asset(
66 'assets/lottie/loading.json', 76 'assets/lottie/loading.json',
67 width: 130, 77 width: 130,
@@ -70,16 +80,12 @@ class TodayHrvChartCard extends StatelessWidget { @@ -70,16 +80,12 @@ class TodayHrvChartCard extends StatelessWidget {
70 animate: true, 80 animate: true,
71 ), 81 ),
72 ) 82 )
73 - : hrvSpots.isEmpty  
74 - ? _EmptyChart(text: context.l10n.noDataAvailableForToday)  
75 - : _HrvLineChart(  
76 - spots: hrvSpots,  
77 - stateMap: hrvStateMap,  
78 - maxSeconds: _maxChartSeconds(),  
79 - getColorByState: _getColorByState,  
80 - getTooltipChildren: _getTooltipChildren,  
81 - timeLabel: _timeLabel,  
82 - ), 83 + else if (hrvSpots.isEmpty)
  84 + _EmptyChart(text: context.l10n.noDataAvailableForToday)
  85 + else
  86 + SizedBox()
  87 + ],
  88 + ),
83 ), 89 ),
84 Padding( 90 Padding(
85 padding: const EdgeInsets.only(bottom: 14, top: 16), 91 padding: const EdgeInsets.only(bottom: 14, top: 16),
@@ -127,23 +133,13 @@ class TodayHrvChartCard extends StatelessWidget { @@ -127,23 +133,13 @@ class TodayHrvChartCard extends StatelessWidget {
127 ), 133 ),
128 SizedBox( 134 SizedBox(
129 height: 190, 135 height: 190,
130 - child: controller.isLoadingToday.value  
131 - ? Center(  
132 - child: Lottie.asset(  
133 - 'assets/lottie/loading.json',  
134 - width: 130,  
135 - height: 90,  
136 - repeat: true,  
137 - animate: true,  
138 - ),  
139 - )  
140 - : _buildStressChart(  
141 - context,  
142 - sleepList,  
143 - stressPoints,  
144 - selectedDate,  
145 - vipInfo?.isVip == true,  
146 - ), 136 + child: _buildStressChart(
  137 + context,
  138 + sleepList,
  139 + stressPoints,
  140 + selectedDate,
  141 + vipInfo?.isVip == true,
  142 + ),
147 ), 143 ),
148 ], 144 ],
149 ), 145 ),
@@ -255,20 +251,31 @@ class TodayHrvChartCard extends StatelessWidget { @@ -255,20 +251,31 @@ class TodayHrvChartCard extends StatelessWidget {
255 } 251 }
256 return Stack( 252 return Stack(
257 children: [ 253 children: [
258 - if (stressPoints.isEmpty) 254 + _StressBarChart(
  255 + sleepList: sleepList,
  256 + stressPoints: stressPoints,
  257 + selectedDate: selectedDate,
  258 + isVip: isVip,
  259 + onTapPurchase: onTapPurchase,
  260 + maxSeconds: _maxChartSeconds(),
  261 + getColorByState: _getColorByState,
  262 + getTooltip: _getTooltip,
  263 + timeLabel: _timeLabel,
  264 + ),
  265 + if (controller.isLoadingToday.value)
  266 + Center(
  267 + child: Lottie.asset(
  268 + 'assets/lottie/loading.json',
  269 + width: 130,
  270 + height: 90,
  271 + repeat: true,
  272 + animate: true,
  273 + ),
  274 + )
  275 + else if (stressPoints.isEmpty)
259 _EmptyChart(text: context.l10n.noPressureDataAvailableAtThisTime) 276 _EmptyChart(text: context.l10n.noPressureDataAvailableAtThisTime)
260 else 277 else
261 - _StressBarChart(  
262 - sleepList: sleepList,  
263 - stressPoints: stressPoints,  
264 - selectedDate: selectedDate,  
265 - isVip: isVip,  
266 - onTapPurchase: onTapPurchase,  
267 - maxSeconds: _maxChartSeconds(),  
268 - getColorByState: _getColorByState,  
269 - getTooltip: _getTooltip,  
270 - timeLabel: _timeLabel,  
271 - ), 278 + SizedBox(),
272 if (isVip != true) ...[ 279 if (isVip != true) ...[
273 GestureDetector( 280 GestureDetector(
274 onTap: onTapPurchase, 281 onTap: onTapPurchase,
@@ -26,6 +26,9 @@ class TodayHrvNumberCard extends StatelessWidget { @@ -26,6 +26,9 @@ class TodayHrvNumberCard extends StatelessWidget {
26 var otherHrv = (sameDay 26 var otherHrv = (sameDay
27 ? context.l10n.todaySAverageHrv 27 ? context.l10n.todaySAverageHrv
28 : context.l10n.averageHrvForTheDay); 28 : context.l10n.averageHrvForTheDay);
  29 +
  30 + var hrvAvg = controller.v2HealthData.value?.hrvAvg ?? 0;
  31 +
29 return Row( 32 return Row(
30 children: [ 33 children: [
31 // HRV 34 // HRV
@@ -34,7 +37,7 @@ class TodayHrvNumberCard extends StatelessWidget { @@ -34,7 +37,7 @@ class TodayHrvNumberCard extends StatelessWidget {
34 label: controller.isFriend 37 label: controller.isFriend
35 ? context.l10n.reportOtherPossessiveTitle(otherHrv) 38 ? context.l10n.reportOtherPossessiveTitle(otherHrv)
36 : otherHrv, 39 : otherHrv,
37 - value: controller.v2HealthData.value?.hrvAvg?.toString() ?? '-', 40 + value: hrvAvg > 0 ? hrvAvg.toString() : '-',
38 unit: 'ms', 41 unit: 'ms',
39 ), 42 ),
40 ), 43 ),
@@ -300,14 +300,15 @@ class TodayActivityCard extends StatelessWidget { @@ -300,14 +300,15 @@ class TodayActivityCard extends StatelessWidget {
300 ), 300 ),
301 children: [ 301 children: [
302 TextSpan(text: activityStr), 302 TextSpan(text: activityStr),
303 - TextSpan(  
304 - text: ' ${l10n.reportUnitKcal}',  
305 - style: TextStyle(  
306 - color: context.colors.textSecondary,  
307 - fontSize: 12,  
308 - fontWeight: FontWeight.w400, 303 + if (activityStr != '-')
  304 + TextSpan(
  305 + text: ' ${l10n.reportUnitKcal}',
  306 + style: TextStyle(
  307 + color: context.colors.textSecondary,
  308 + fontSize: 12,
  309 + fontWeight: FontWeight.w400,
  310 + ),
309 ), 311 ),
310 - ),  
311 ], 312 ],
312 ), 313 ),
313 ), 314 ),
@@ -39,118 +39,128 @@ class MembershipOfferView extends GetView<MembershipOfferController> { @@ -39,118 +39,128 @@ class MembershipOfferView extends GetView<MembershipOfferController> {
39 controller.unlock(); 39 controller.unlock();
40 }, 40 },
41 ), 41 ),
42 - child: SingleChildScrollView(  
43 - physics: const ClampingScrollPhysics(),  
44 - padding: EdgeInsets.fromLTRB(  
45 - 0,  
46 - MediaQuery.paddingOf(context).top + 4,  
47 - 0,  
48 - 0,  
49 - ),  
50 - child: Stack(  
51 - children: [  
52 - IgnorePointer(  
53 - child: Lottie.asset('assets/lottie/scatter_flowers.json'),  
54 - ),  
55 - Column(  
56 - mainAxisSize: MainAxisSize.max,  
57 - children: [  
58 - Padding(  
59 - padding: const EdgeInsets.symmetric(horizontal: 16),  
60 - child: Column(  
61 - children: [  
62 - SizedBox(height: 48),  
63 - OnboardingTitleText(l10n.onboardingMemberTitle),  
64 - const SizedBox(height: 12),  
65 - OnboardingBodyText(  
66 - l10n.onboardingMemberBody,  
67 - fontSize: 14,  
68 - ),  
69 - ], 42 + child: Column(
  43 + children: [
  44 + Expanded(
  45 + child: SingleChildScrollView(
  46 + physics: const ClampingScrollPhysics(),
  47 + padding: EdgeInsets.fromLTRB(
  48 + 0,
  49 + MediaQuery.paddingOf(context).top + 4,
  50 + 0,
  51 + 0,
  52 + ),
  53 + child: Stack(
  54 + children: [
  55 + IgnorePointer(
  56 + child: Lottie.asset(
  57 + 'assets/lottie/scatter_flowers.json',
  58 + width: Get.width),
70 ), 59 ),
71 - ),  
72 - const SizedBox(height: 12),  
73 - Image.asset(  
74 - 'assets/images/user_onboarding/ic_membership_box.png'),  
75 - const SizedBox(height: 4),  
76 - Obx(() {  
77 - final yearInfo = controller.yearlyProductAppleInfo.value;  
78 - final yearProduct = controller.yearlyProduct.value;  
79 -  
80 - return yearInfo == null || yearProduct == null  
81 - ? const SizedBox(child: CircularProgressIndicator())  
82 - : Column( 60 + Column(
  61 + children: [
  62 + Padding(
  63 + padding: const EdgeInsets.symmetric(horizontal: 16),
  64 + child: Column(
83 children: [ 65 children: [
84 - Text(  
85 - '原价${_originDisplayPrice(yearInfo)}/年',  
86 - textAlign: TextAlign.center,  
87 - style: const TextStyle(  
88 - color: Color(0xFFB0B0B6),  
89 - fontSize: 16,  
90 - decoration: TextDecoration.lineThrough,  
91 - decorationColor: Color(0xFFB0B0B6),  
92 - letterSpacing: 0,  
93 - ),  
94 - ),  
95 - const SizedBox(height: 6),  
96 - Text(  
97 - _displayPrice(yearInfo),  
98 - textAlign: TextAlign.center,  
99 - style: TextStyle(  
100 - color: context.colors.primary,  
101 - fontSize: 28,  
102 - fontWeight: FontWeight.w600,  
103 - letterSpacing: 0,  
104 - ),  
105 - ),  
106 - const SizedBox(height: 6),  
107 - Text(  
108 - _planSubtitle(yearProduct, yearInfo),  
109 - textAlign: TextAlign.center,  
110 - style: const TextStyle(  
111 - color: Color(0xFFB0B0B6),  
112 - fontSize: 12,  
113 - letterSpacing: 0,  
114 - ), 66 + SizedBox(height: 48),
  67 + OnboardingTitleText(l10n.onboardingMemberTitle),
  68 + const SizedBox(height: 12),
  69 + OnboardingBodyText(
  70 + l10n.onboardingMemberBody,
  71 + fontSize: 14,
115 ), 72 ),
116 ], 73 ],
117 - );  
118 - }),  
119 - const SizedBox(height: 58),  
120 - GestureDetector(  
121 - behavior: HitTestBehavior.opaque,  
122 - onTap: () {  
123 - var arguments = {};  
124 - if (controller.isFromOnboard) {  
125 - arguments['from'] = 'onboarding';  
126 - } else {  
127 - arguments['channel_type'] =  
128 - Get.arguments?['channel_type'] ?? '';  
129 - }  
130 - Get.offNamed(Routes.PURCHASE, arguments: arguments);  
131 - },  
132 - child: Padding(  
133 - padding: const EdgeInsets.symmetric(  
134 - vertical: 12,  
135 - horizontal: 24,  
136 - ),  
137 - child: Text(  
138 - l10n.onboardingMemberAllOptions,  
139 - textAlign: TextAlign.center,  
140 - style: TextStyle(  
141 - color: context.colors.textPrimary,  
142 - fontSize: 12,  
143 - decoration: TextDecoration.underline,  
144 - decorationColor: context.colors.textPrimary,  
145 - letterSpacing: 0, 74 + ),
146 ), 75 ),
147 - ), 76 + const SizedBox(height: 12),
  77 + Image.asset(
  78 + 'assets/images/user_onboarding/ic_membership_box.png'),
  79 + const SizedBox(height: 4),
  80 + Obx(() {
  81 + final yearInfo =
  82 + controller.yearlyProductAppleInfo.value;
  83 + final yearProduct = controller.yearlyProduct.value;
  84 +
  85 + return yearInfo == null || yearProduct == null
  86 + ? const SizedBox(
  87 + child: CircularProgressIndicator())
  88 + : Column(
  89 + children: [
  90 + Text(
  91 + '原价${_originDisplayPrice(yearInfo)}/年',
  92 + textAlign: TextAlign.center,
  93 + style: const TextStyle(
  94 + color: Color(0xFF78787D),
  95 + fontSize: 16,
  96 + decoration:
  97 + TextDecoration.lineThrough,
  98 + decorationColor: Color(0xFF78787D),
  99 + letterSpacing: 0,
  100 + ),
  101 + ),
  102 + const SizedBox(height: 6),
  103 + Text(
  104 + _displayPrice(yearInfo),
  105 + textAlign: TextAlign.center,
  106 + style: TextStyle(
  107 + color: context.colors.primary,
  108 + fontSize: 28,
  109 + fontWeight: FontWeight.w600,
  110 + letterSpacing: 0,
  111 + ),
  112 + ),
  113 + const SizedBox(height: 6),
  114 + Text(
  115 + _planSubtitle(yearProduct, yearInfo),
  116 + textAlign: TextAlign.center,
  117 + style: const TextStyle(
  118 + color: Color(0xFF78787D),
  119 + fontSize: 12,
  120 + letterSpacing: 0,
  121 + ),
  122 + ),
  123 + ],
  124 + );
  125 + }),
  126 + ],
148 ), 127 ),
  128 + ],
  129 + ),
  130 + ),
  131 + ),
  132 + GestureDetector(
  133 + behavior: HitTestBehavior.opaque,
  134 + onTap: () {
  135 + var arguments = {};
  136 + if (controller.isFromOnboard) {
  137 + arguments['from'] = 'onboarding';
  138 + } else {
  139 + arguments['channel_type'] =
  140 + Get.arguments?['channel_type'] ?? '';
  141 + }
  142 + Get.offNamed(Routes.PURCHASE, arguments: arguments);
  143 + },
  144 + child: Padding(
  145 + padding: const EdgeInsets.symmetric(
  146 + vertical: 12,
  147 + horizontal: 24,
  148 + ),
  149 + child: Text(
  150 + l10n.onboardingMemberAllOptions,
  151 + textAlign: TextAlign.center,
  152 + style: TextStyle(
  153 + color: context.colors.textPrimary,
  154 + fontSize: 12,
  155 + decoration: TextDecoration.underline,
  156 + decorationColor: context.colors.textPrimary,
  157 + letterSpacing: 0,
149 ), 158 ),
150 - ], 159 + ),
151 ), 160 ),
152 - ],  
153 - ), 161 + ),
  162 + const SizedBox(height: 140),
  163 + ],
154 ), 164 ),
155 )); 165 ));
156 } 166 }
@@ -269,6 +269,7 @@ class _FaqLink extends StatelessWidget { @@ -269,6 +269,7 @@ class _FaqLink extends StatelessWidget {
269 fontSize: 12, 269 fontSize: 12,
270 height: 17 / 12, 270 height: 17 / 12,
271 decoration: TextDecoration.underline, 271 decoration: TextDecoration.underline,
  272 + decorationColor: context.colors.textSecondary,
272 ), 273 ),
273 ), 274 ),
274 ), 275 ),
@@ -139,8 +139,9 @@ class _IntroPage extends StatelessWidget { @@ -139,8 +139,9 @@ class _IntroPage extends StatelessWidget {
139 const SizedBox(height: 12), 139 const SizedBox(height: 12),
140 Image.asset( 140 Image.asset(
141 'assets/images/user_onboarding/bg_onboarding_watch.png', 141 'assets/images/user_onboarding/bg_onboarding_watch.png',
142 - height: 384,  
143 - fit: BoxFit.fitHeight, 142 + height: Get.width / 375 * 384,
  143 + width: Get.width,
  144 + fit: BoxFit.cover,
144 ), 145 ),
145 const SizedBox(height: 12), 146 const SizedBox(height: 12),
146 OnboardingEmRichText( 147 OnboardingEmRichText(
@@ -284,13 +285,15 @@ class _HrvIntroPageState extends State<_HrvIntroPage> @@ -284,13 +285,15 @@ class _HrvIntroPageState extends State<_HrvIntroPage>
284 _isRepeat 285 _isRepeat
285 ? Lottie.asset( 286 ? Lottie.asset(
286 'assets/lottie/onboarding_hrv_repeat.json', 287 'assets/lottie/onboarding_hrv_repeat.json',
287 - height: 210, 288 + height: Get.width / 375 * 210,
  289 + width: Get.width,
288 fit: BoxFit.cover, 290 fit: BoxFit.cover,
289 repeat: true, 291 repeat: true,
290 ) 292 )
291 : Lottie.asset( 293 : Lottie.asset(
292 'assets/lottie/onboarding_hrv_once.json', 294 'assets/lottie/onboarding_hrv_once.json',
293 - height: 210, 295 + height: Get.width / 375 * 210,
  296 + width: Get.width,
294 fit: BoxFit.cover, 297 fit: BoxFit.cover,
295 controller: _controller, 298 controller: _controller,
296 onLoaded: (composition) { 299 onLoaded: (composition) {
@@ -301,7 +304,7 @@ class _HrvIntroPageState extends State<_HrvIntroPage> @@ -301,7 +304,7 @@ class _HrvIntroPageState extends State<_HrvIntroPage>
301 ), 304 ),
302 const SizedBox(height: 31), 305 const SizedBox(height: 31),
303 Padding( 306 Padding(
304 - padding: const EdgeInsets.symmetric(horizontal: 16), 307 + padding: const EdgeInsets.symmetric(horizontal: 48),
305 child: Text( 308 child: Text(
306 l10n.onboardingHrvDescription, 309 l10n.onboardingHrvDescription,
307 textAlign: TextAlign.center, 310 textAlign: TextAlign.center,
@@ -457,62 +460,87 @@ class _HrvResearchPageState extends State<_HrvResearchPage> { @@ -457,62 +460,87 @@ class _HrvResearchPageState extends State<_HrvResearchPage> {
457 ), 460 ),
458 ]; 461 ];
459 462
460 - return OnboardingPageScrollBody(  
461 - horizontalPadding: 16,  
462 - child: Column(  
463 - children: [  
464 - Padding(  
465 - padding: const EdgeInsets.symmetric(horizontal: 16),  
466 - child: OnboardingTitleText(  
467 - l10n.onboardingResearchTitle,  
468 - maxWidth: 320, 463 + final topPadding = MediaQuery.paddingOf(context).top + 4;
  464 +
  465 + return LayoutBuilder(
  466 + builder: (context, constraints) {
  467 + return SingleChildScrollView(
  468 + physics: const ClampingScrollPhysics(),
  469 + padding: EdgeInsets.fromLTRB(16, topPadding, 16, 140),
  470 + child: ConstrainedBox(
  471 + constraints: BoxConstraints(
  472 + minHeight: constraints.maxHeight - topPadding - 140,
469 ), 473 ),
470 - ),  
471 - const SizedBox(height: 24),  
472 - // ── Pager 区域 ──  
473 - NotificationListener<ScrollNotification>(  
474 - onNotification: (notification) {  
475 - if (notification is ScrollStartNotification &&  
476 - notification.dragDetails != null) {  
477 - // 用户开始拖拽,暂停自动播放  
478 - if (!_userScrolling) {  
479 - setState(() => _userScrolling = true);  
480 - _stopAutoScroll();  
481 - }  
482 - } else if (notification is ScrollEndNotification) {  
483 - // 手势结束,恢复自动播放  
484 - if (_userScrolling) {  
485 - setState(() => _userScrolling = false);  
486 - _startAutoScroll();  
487 - }  
488 - }  
489 - return false;  
490 - },  
491 - child: SizedBox(  
492 - height: 361,  
493 - child: PageView.builder(  
494 - controller: _pageController,  
495 - itemCount: _virtualCount,  
496 - onPageChanged: (_) => setState(() {}),  
497 - itemBuilder: (context, virtualIndex) {  
498 - final realIndex = virtualIndex % _realCount;  
499 - return pages[realIndex];  
500 - }, 474 + child: IntrinsicHeight(
  475 + child: Column(
  476 + children: [
  477 + Padding(
  478 + padding: const EdgeInsets.symmetric(horizontal: 16),
  479 + child: OnboardingTitleText(
  480 + l10n.onboardingResearchTitle,
  481 + maxWidth: 320,
  482 + ),
  483 + ),
  484 + SizedBox(
  485 + height: 16,
  486 + ),
  487 + Expanded(
  488 + child: Center(
  489 + child: Column(
  490 + mainAxisSize: MainAxisSize.min,
  491 + children: [
  492 + NotificationListener<ScrollNotification>(
  493 + onNotification: (notification) {
  494 + if (notification is ScrollStartNotification &&
  495 + notification.dragDetails != null) {
  496 + // 用户开始拖拽,暂停自动播放
  497 + if (!_userScrolling) {
  498 + setState(() => _userScrolling = true);
  499 + _stopAutoScroll();
  500 + }
  501 + } else if (notification
  502 + is ScrollEndNotification) {
  503 + // 手势结束,恢复自动播放
  504 + if (_userScrolling) {
  505 + setState(() => _userScrolling = false);
  506 + _startAutoScroll();
  507 + }
  508 + }
  509 + return false;
  510 + },
  511 + child: SizedBox(
  512 + height: 361,
  513 + child: PageView.builder(
  514 + controller: _pageController,
  515 + itemCount: _virtualCount,
  516 + onPageChanged: (_) => setState(() {}),
  517 + itemBuilder: (context, virtualIndex) {
  518 + final realIndex = virtualIndex % _realCount;
  519 + return pages[realIndex];
  520 + },
  521 + ),
  522 + ),
  523 + ),
  524 + const SizedBox(height: 20),
  525 + AnimatedBuilder(
  526 + animation: _pageController,
  527 + builder: (context, _) {
  528 + return OnboardingPageIndicators(
  529 + activeIndex: _activeRealIndex,
  530 + count: _realCount,
  531 + );
  532 + },
  533 + ),
  534 + ],
  535 + ),
  536 + ),
  537 + ),
  538 + ],
501 ), 539 ),
502 ), 540 ),
503 ), 541 ),
504 - const SizedBox(height: 20),  
505 - AnimatedBuilder(  
506 - animation: _pageController,  
507 - builder: (context, _) {  
508 - return OnboardingPageIndicators(  
509 - activeIndex: _activeRealIndex,  
510 - count: _realCount,  
511 - );  
512 - },  
513 - ),  
514 - ],  
515 - ), 542 + );
  543 + },
516 ); 544 );
517 } 545 }
518 } 546 }
@@ -564,8 +592,9 @@ class HealthPermissionPage extends StatelessWidget { @@ -564,8 +592,9 @@ class HealthPermissionPage extends StatelessWidget {
564 const SizedBox(height: 28), 592 const SizedBox(height: 28),
565 Image.asset( 593 Image.asset(
566 'assets/images/permission/apple_open_health_kit_cn.png', 594 'assets/images/permission/apple_open_health_kit_cn.png',
567 - height: 295,  
568 - fit: BoxFit.fitHeight, 595 + height: (Get.width - 80) / 295 * 360,
  596 + width: Get.width - 80,
  597 + fit: BoxFit.cover,
569 ), 598 ),
570 const SizedBox(height: 16), 599 const SizedBox(height: 16),
571 Padding( 600 Padding(
@@ -20,16 +20,30 @@ class ResearchCard extends StatelessWidget { @@ -20,16 +20,30 @@ class ResearchCard extends StatelessWidget {
20 Widget build(BuildContext context) { 20 Widget build(BuildContext context) {
21 return Container( 21 return Container(
22 height: 320, 22 height: 320,
23 - width: 171, 23 + width: 172,
24 decoration: BoxDecoration( 24 decoration: BoxDecoration(
25 color: backgroundColor, 25 color: backgroundColor,
26 borderRadius: BorderRadius.circular(32), 26 borderRadius: BorderRadius.circular(32),
27 - image: DecorationImage(image: AssetImage(image), fit: BoxFit.fill),  
28 ), 27 ),
29 padding: const EdgeInsets.fromLTRB(12, 24, 12, 28), 28 padding: const EdgeInsets.fromLTRB(12, 24, 12, 28),
30 child: Stack( 29 child: Stack(
31 children: [ 30 children: [
32 Positioned( 31 Positioned(
  32 + bottom: 0,
  33 + left: 0,
  34 + right: 0,
  35 + child: Container(
  36 + width: 172,
  37 + height: 212,
  38 + decoration: BoxDecoration(
  39 + color: backgroundColor,
  40 + borderRadius: BorderRadius.circular(32),
  41 + image: DecorationImage(
  42 + image: AssetImage(image), fit: BoxFit.cover),
  43 + ),
  44 + ),
  45 + ),
  46 + Positioned(
33 top: 0, 47 top: 0,
34 left: 0, 48 left: 0,
35 right: 0, 49 right: 0,