Showing
18 changed files
with
336 additions
and
239 deletions
| @@ -84,7 +84,7 @@ class BindPartnerController extends GetxController { | @@ -84,7 +84,7 @@ class BindPartnerController extends GetxController { | ||
| 84 | 84 | ||
| 85 | void onShareMyCode() async { | 85 | void onShareMyCode() async { |
| 86 | try { | 86 | try { |
| 87 | - ta.track('enter_doublefeel_add_friend_page', | 87 | + ta.track('click_doublefeel_add_friend_page', |
| 88 | properties: {'channel_type': channel, 'click_content': '分享我的码'}); | 88 | properties: {'channel_type': channel, 'click_content': '分享我的码'}); |
| 89 | final success = await PlatformHostApi() | 89 | final success = await PlatformHostApi() |
| 90 | .shareText(l10n.sharePartnerCodeTemplate(myInviteCode.value)); | 90 | .shareText(l10n.sharePartnerCodeTemplate(myInviteCode.value)); |
| @@ -94,7 +94,7 @@ class BindPartnerController extends GetxController { | @@ -94,7 +94,7 @@ class BindPartnerController extends GetxController { | ||
| 94 | } | 94 | } |
| 95 | 95 | ||
| 96 | Future<void> onSubmitPartnerId() async { | 96 | Future<void> onSubmitPartnerId() async { |
| 97 | - ta.track('enter_doublefeel_add_friend_page', | 97 | + ta.track('click_doublefeel_add_friend_page', |
| 98 | properties: {'channel_type': channel, 'click_content': '继续'}); | 98 | properties: {'channel_type': channel, 'click_content': '继续'}); |
| 99 | if (!canSubmit || isSubmitting.value) return; | 99 | if (!canSubmit || isSubmitting.value) return; |
| 100 | isSubmitting.value = true; | 100 | isSubmitting.value = true; |
| @@ -152,7 +152,7 @@ class BindPartnerController extends GetxController { | @@ -152,7 +152,7 @@ class BindPartnerController extends GetxController { | ||
| 152 | } | 152 | } |
| 153 | 153 | ||
| 154 | Future<void> onSkip() async { | 154 | Future<void> onSkip() async { |
| 155 | - ta.track('enter_doublefeel_add_friend_page', | 155 | + ta.track('click_doublefeel_add_friend_page', |
| 156 | properties: {'channel_type': channel, 'click_content': '下次再说'}); | 156 | properties: {'channel_type': channel, 'click_content': '下次再说'}); |
| 157 | final userStateService = Get.find<UserStateService>(); | 157 | final userStateService = Get.find<UserStateService>(); |
| 158 | if (!userStateService.isVip) { | 158 | if (!userStateService.isVip) { |
| @@ -44,33 +44,13 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -44,33 +44,13 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 44 | physics: const ClampingScrollPhysics(), | 44 | physics: const ClampingScrollPhysics(), |
| 45 | children: [ | 45 | children: [ |
| 46 | _buildTitle(context), | 46 | _buildTitle(context), |
| 47 | - SizedBox(height: 28), | 47 | + // SizedBox(height: 28), |
| 48 | _buildCardArea(context), | 48 | _buildCardArea(context), |
| 49 | - SizedBox(height: 58), | 49 | + // SizedBox(height: 58), |
| 50 | if (controller.isShowSkipButton) _buildSkipButton(context), | 50 | if (controller.isShowSkipButton) _buildSkipButton(context), |
| 51 | ], | 51 | ], |
| 52 | ), | 52 | ), |
| 53 | ), | 53 | ), |
| 54 | - Positioned( | ||
| 55 | - top: MediaQuery.paddingOf(context).top + 92, | ||
| 56 | - left: 0, | ||
| 57 | - child: Image.asset( | ||
| 58 | - 'assets/images/user_onboarding/ic_bind_decor_top.png', | ||
| 59 | - width: 133, | ||
| 60 | - height: 124, | ||
| 61 | - fit: BoxFit.fitHeight, | ||
| 62 | - ), | ||
| 63 | - ), | ||
| 64 | - Positioned( | ||
| 65 | - top: MediaQuery.paddingOf(context).top + 532, | ||
| 66 | - right: 0, | ||
| 67 | - child: Image.asset( | ||
| 68 | - 'assets/images/user_onboarding/ic_bind_decor_bottom.png', | ||
| 69 | - width: 133, | ||
| 70 | - height: 124, | ||
| 71 | - fit: BoxFit.fitHeight, | ||
| 72 | - ), | ||
| 73 | - ), | ||
| 74 | ], | 54 | ], |
| 75 | ), | 55 | ), |
| 76 | ), | 56 | ), |
| @@ -116,48 +96,73 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -116,48 +96,73 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 116 | } | 96 | } |
| 117 | 97 | ||
| 118 | Widget _buildCardArea(BuildContext context) { | 98 | Widget _buildCardArea(BuildContext context) { |
| 119 | - return Container( | ||
| 120 | - margin: EdgeInsets.symmetric(horizontal: 24), | ||
| 121 | - decoration: BoxDecoration( | ||
| 122 | - borderRadius: BorderRadius.circular(20), | ||
| 123 | - image: DecorationImage( | ||
| 124 | - image: | ||
| 125 | - AssetImage('assets/images/friends/bg_friend_add_subtract.webp'), | ||
| 126 | - fit: BoxFit.cover, | 99 | + return Stack( |
| 100 | + children: [ | ||
| 101 | + Container( | ||
| 102 | + margin: EdgeInsets.only(top: 28, bottom: 64, left: 24, right: 24), | ||
| 103 | + decoration: BoxDecoration( | ||
| 104 | + borderRadius: BorderRadius.circular(20), | ||
| 105 | + image: DecorationImage( | ||
| 106 | + image: AssetImage( | ||
| 107 | + 'assets/images/friends/bg_friend_add_subtract.webp'), | ||
| 108 | + fit: BoxFit.cover, | ||
| 109 | + ), | ||
| 110 | + boxShadow: [ | ||
| 111 | + BoxShadow( | ||
| 112 | + color: context.colors.primary.withValues(alpha: 0.08), | ||
| 113 | + blurRadius: 24, | ||
| 114 | + offset: const Offset(0, 8), | ||
| 115 | + ), | ||
| 116 | + ], | ||
| 117 | + ), | ||
| 118 | + child: Column( | ||
| 119 | + children: [ | ||
| 120 | + SizedBox(height: 28), | ||
| 121 | + _buildMyInfoSection(context), | ||
| 122 | + SizedBox(height: 40), | ||
| 123 | + _buildOrDivider(context), | ||
| 124 | + SizedBox(height: 28), | ||
| 125 | + _buildInputSection(context), | ||
| 126 | + SizedBox(height: 16), | ||
| 127 | + // 继续按钮 | ||
| 128 | + Container( | ||
| 129 | + padding: EdgeInsets.symmetric(horizontal: 32), | ||
| 130 | + child: Obx(() { | ||
| 131 | + return _buildPrimaryButton( | ||
| 132 | + label: context.l10n.continueButton, | ||
| 133 | + onTap: controller.onSubmitPartnerId, | ||
| 134 | + enabled: | ||
| 135 | + controller.canSubmit && !controller.isSubmitting.value, | ||
| 136 | + isLoading: controller.isSubmitting.value, | ||
| 137 | + primaryColor: context.colors.chartOrange, | ||
| 138 | + ); | ||
| 139 | + }), | ||
| 140 | + ), | ||
| 141 | + SizedBox(height: 40), | ||
| 142 | + ], | ||
| 143 | + ), | ||
| 127 | ), | 144 | ), |
| 128 | - boxShadow: [ | ||
| 129 | - BoxShadow( | ||
| 130 | - color: context.colors.primary.withValues(alpha: 0.08), | ||
| 131 | - blurRadius: 24, | ||
| 132 | - offset: const Offset(0, 8), | 145 | + Positioned( |
| 146 | + top: 0, | ||
| 147 | + left: 0, | ||
| 148 | + child: Image.asset( | ||
| 149 | + 'assets/images/user_onboarding/ic_bind_decor_top.png', | ||
| 150 | + width: 133, | ||
| 151 | + height: 124, | ||
| 152 | + fit: BoxFit.fitHeight, | ||
| 133 | ), | 153 | ), |
| 134 | - ], | ||
| 135 | - ), | ||
| 136 | - child: Column( | ||
| 137 | - children: [ | ||
| 138 | - SizedBox(height: 28), | ||
| 139 | - _buildMyInfoSection(context), | ||
| 140 | - SizedBox(height: 40), | ||
| 141 | - _buildOrDivider(context), | ||
| 142 | - SizedBox(height: 28), | ||
| 143 | - _buildInputSection(context), | ||
| 144 | - SizedBox(height: 16), | ||
| 145 | - // 继续按钮 | ||
| 146 | - Container( | ||
| 147 | - padding: EdgeInsets.symmetric(horizontal: 32), | ||
| 148 | - child: Obx(() { | ||
| 149 | - return _buildPrimaryButton( | ||
| 150 | - label: context.l10n.continueButton, | ||
| 151 | - onTap: controller.onSubmitPartnerId, | ||
| 152 | - enabled: controller.canSubmit && !controller.isSubmitting.value, | ||
| 153 | - isLoading: controller.isSubmitting.value, | ||
| 154 | - primaryColor: context.colors.chartOrange, | ||
| 155 | - ); | ||
| 156 | - }), | 154 | + ), |
| 155 | + Positioned( | ||
| 156 | + bottom: 0, | ||
| 157 | + right: 0, | ||
| 158 | + child: Image.asset( | ||
| 159 | + 'assets/images/user_onboarding/ic_bind_decor_bottom.png', | ||
| 160 | + width: 133, | ||
| 161 | + height: 124, | ||
| 162 | + fit: BoxFit.fitHeight, | ||
| 157 | ), | 163 | ), |
| 158 | - SizedBox(height: 40), | ||
| 159 | - ], | ||
| 160 | - ), | 164 | + ), |
| 165 | + ], | ||
| 161 | ); | 166 | ); |
| 162 | } | 167 | } |
| 163 | 168 | ||
| @@ -199,7 +204,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -199,7 +204,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 199 | ClipboardData(text: controller.myInviteCode.value), | 204 | ClipboardData(text: controller.myInviteCode.value), |
| 200 | ); | 205 | ); |
| 201 | AppToast.show('复制成功'); | 206 | AppToast.show('复制成功'); |
| 202 | - ta.track('enter_doublefeel_add_friend_page', properties: { | 207 | + ta.track('click_doublefeel_add_friend_page', properties: { |
| 203 | 'channel_type': controller.channel, | 208 | 'channel_type': controller.channel, |
| 204 | 'click_content': '复制共享码' | 209 | 'click_content': '复制共享码' |
| 205 | }); | 210 | }); |
| @@ -294,24 +299,45 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -294,24 +299,45 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 294 | border: Border.all(color: const Color(0xFFF0F0F6), width: 1), | 299 | border: Border.all(color: const Color(0xFFF0F0F6), width: 1), |
| 295 | borderRadius: BorderRadius.circular(27), | 300 | borderRadius: BorderRadius.circular(27), |
| 296 | ), | 301 | ), |
| 297 | - child: TextField( | ||
| 298 | - controller: controller.partnerIdController, | ||
| 299 | - textAlign: TextAlign.center, | ||
| 300 | - style: TextStyle( | ||
| 301 | - fontSize: 32, | ||
| 302 | - fontWeight: FontWeight.w400, | ||
| 303 | - color: context.colors.textPrimary, | ||
| 304 | - ), | ||
| 305 | - decoration: InputDecoration( | ||
| 306 | - hintText: context.l10n.bindPartnerInputHint, | ||
| 307 | - hintStyle: TextStyle( | ||
| 308 | - fontSize: 16, | ||
| 309 | - fontWeight: FontWeight.w400, | ||
| 310 | - color: const Color(0xFFD0D0D2), | 302 | + child: Stack( |
| 303 | + alignment: Alignment.center, // 确保 Stack 里的子项绝对居中 | ||
| 304 | + children: [ | ||
| 305 | + // 自定义 Hint 文字层 | ||
| 306 | + Obx(() => controller.partnerIdInput.value.isEmpty | ||
| 307 | + ? IgnorePointer( | ||
| 308 | + child: Text( | ||
| 309 | + context.l10n.bindPartnerInputHint, | ||
| 310 | + style: const TextStyle( | ||
| 311 | + fontSize: 16, | ||
| 312 | + fontWeight: FontWeight.w400, | ||
| 313 | + color: Color(0xFFD0D0D2), | ||
| 314 | + height: 1.1, | ||
| 315 | + ), | ||
| 316 | + ), | ||
| 317 | + ) | ||
| 318 | + : const SizedBox()), | ||
| 319 | + // 真正的输入框层(不设 hintText) | ||
| 320 | + TextField( | ||
| 321 | + controller: controller.partnerIdController, | ||
| 322 | + textAlign: TextAlign.center, | ||
| 323 | + textAlignVertical: TextAlignVertical.center, | ||
| 324 | + style: TextStyle( | ||
| 325 | + fontSize: 32, | ||
| 326 | + fontWeight: FontWeight.w400, | ||
| 327 | + color: context.colors.textPrimary, | ||
| 328 | + height: 1.1, | ||
| 329 | + ), | ||
| 330 | + cursorWidth: 2.0, | ||
| 331 | + cursorHeight: 24.0, | ||
| 332 | + cursorRadius: const Radius.circular(1.5), | ||
| 333 | + decoration: const InputDecoration( | ||
| 334 | + border: InputBorder.none, | ||
| 335 | + isCollapsed: true, | ||
| 336 | + contentPadding: | ||
| 337 | + EdgeInsets.symmetric(horizontal: 20, vertical: 0), | ||
| 338 | + ), | ||
| 311 | ), | 339 | ), |
| 312 | - border: InputBorder.none, | ||
| 313 | - contentPadding: EdgeInsets.symmetric(horizontal: 20), | ||
| 314 | - ), | 340 | + ], |
| 315 | ), | 341 | ), |
| 316 | ), | 342 | ), |
| 317 | ], | 343 | ], |
| @@ -7,6 +7,7 @@ import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_cont | @@ -7,6 +7,7 @@ import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_cont | ||
| 7 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/friend_select_bottom_sheet.dart'; | 7 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/friend_select_bottom_sheet.dart'; |
| 8 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; | 8 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; |
| 9 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 9 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 10 | +import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | ||
| 10 | import 'package:doublefeel_flutter/core/constants/membership.dart'; | 11 | import 'package:doublefeel_flutter/core/constants/membership.dart'; |
| 11 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | 12 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; |
| 12 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 13 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| @@ -190,35 +191,42 @@ class TodayController extends GetMaterialController { | @@ -190,35 +191,42 @@ class TodayController extends GetMaterialController { | ||
| 190 | } | 191 | } |
| 191 | 192 | ||
| 192 | void checkAddFriendVisible() { | 193 | void checkAddFriendVisible() { |
| 193 | - _refreshFriendList(onFriendListUpdated: () { | ||
| 194 | - if (friendsList.isNotEmpty || | ||
| 195 | - friendsList.value.length >= friendsListLimit) { | ||
| 196 | - showPartnerAdBanner.value = false; | ||
| 197 | - return; | ||
| 198 | - } | ||
| 199 | - // 关闭后 3 天内不再显示 | ||
| 200 | - final userId = | ||
| 201 | - Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ?? | ||
| 202 | - 0; | ||
| 203 | - final lastShowTime = | ||
| 204 | - Get.find<UserAccountStorage>().getLastAddFriendBannerShowTime(userId); | ||
| 205 | - if (lastShowTime != null) { | ||
| 206 | - final diff = DateTime.now() | ||
| 207 | - .difference(DateTime.fromMillisecondsSinceEpoch(lastShowTime)); | ||
| 208 | - if (diff.inDays < 3) { | 194 | + if (isFriend) { |
| 195 | + showPartnerAdBanner.value = false; | ||
| 196 | + } else { | ||
| 197 | + _refreshFriendList(onFriendListUpdated: () { | ||
| 198 | + if (friendsList.isNotEmpty || | ||
| 199 | + friendsList.value.length >= friendsListLimit) { | ||
| 209 | showPartnerAdBanner.value = false; | 200 | showPartnerAdBanner.value = false; |
| 210 | return; | 201 | return; |
| 211 | } | 202 | } |
| 212 | - } | ||
| 213 | - showPartnerAdBanner.value = true; | ||
| 214 | - }); | 203 | + // 关闭后 3 天内不再显示 |
| 204 | + final userId = Get.find<UserPreferencesStorage>() | ||
| 205 | + .preferences | ||
| 206 | + .value | ||
| 207 | + .meUserInfo | ||
| 208 | + ?.id ?? | ||
| 209 | + 0; | ||
| 210 | + final lastShowTime = Get.find<UserAccountStorage>() | ||
| 211 | + .getLastAddFriendBannerShowTime(userId); | ||
| 212 | + if (lastShowTime != null) { | ||
| 213 | + final diff = DateTime.now() | ||
| 214 | + .difference(DateTime.fromMillisecondsSinceEpoch(lastShowTime)); | ||
| 215 | + if (diff.inDays < 3) { | ||
| 216 | + showPartnerAdBanner.value = false; | ||
| 217 | + return; | ||
| 218 | + } | ||
| 219 | + } | ||
| 220 | + showPartnerAdBanner.value = true; | ||
| 221 | + }); | ||
| 222 | + } | ||
| 215 | } | 223 | } |
| 216 | 224 | ||
| 217 | void changeDate(DateTime date, {DateTime? focused}) { | 225 | void changeDate(DateTime date, {DateTime? focused}) { |
| 218 | final normalizedDate = _clampDate(date); | 226 | final normalizedDate = _clampDate(date); |
| 219 | if (!DateUtils.isSameDay(date, lastSelectableDay) && | 227 | if (!DateUtils.isSameDay(date, lastSelectableDay) && |
| 220 | !_userStateService.isVip) { | 228 | !_userStateService.isVip) { |
| 221 | - toPremiumPage(); | 229 | + toPremiumPage('今日-查看非当日数据'); |
| 222 | return; | 230 | return; |
| 223 | } | 231 | } |
| 224 | selectedDate.value = normalizedDate; | 232 | selectedDate.value = normalizedDate; |
| @@ -334,20 +342,40 @@ class TodayController extends GetMaterialController { | @@ -334,20 +342,40 @@ class TodayController extends GetMaterialController { | ||
| 334 | 342 | ||
| 335 | Future<void> checkHealthDataAuthCardVisible( | 343 | Future<void> checkHealthDataAuthCardVisible( |
| 336 | [HealthAuthorization? preResult]) async { | 344 | [HealthAuthorization? preResult]) async { |
| 337 | - try { | ||
| 338 | - final result = preResult ?? await _hostApi.checkHealthAppAuthorization(); | ||
| 339 | - ta.setSuperProperties( | ||
| 340 | - {'health_data_permission': result.status.clamp(0, 1)}); | ||
| 341 | - if (result.status == 1) { | ||
| 342 | - await _performDataUpload(); | ||
| 343 | - } | ||
| 344 | - final hasUploaded = | ||
| 345 | - switch (await _healthApi.getHealthDataEverUploaded()) { | ||
| 346 | - AppSuccess(:final data) => data, | ||
| 347 | - AppFailure() => false, | ||
| 348 | - }; | ||
| 349 | - showHealthDataAuthCardStatus.value = !hasUploaded ? -1 : result.status; | ||
| 350 | - } catch (e) {} | 345 | + final firstTime = DateUtils.dateOnly( |
| 346 | + DateTime(lastSelectableDay.year, lastSelectableDay.month - 6, | ||
| 347 | + lastSelectableDay.day), | ||
| 348 | + ); | ||
| 349 | + if (isFriend) { | ||
| 350 | + showHealthDataAuthCardStatus.value = 1; | ||
| 351 | + firstSelectableDay.value = firstTime; | ||
| 352 | + } else { | ||
| 353 | + try { | ||
| 354 | + final result = | ||
| 355 | + preResult ?? await _hostApi.checkHealthAppAuthorization(); | ||
| 356 | + ta.setSuperProperties( | ||
| 357 | + {'health_data_permission': result.status.clamp(0, 1)}); | ||
| 358 | + if (result.status == 1) { | ||
| 359 | + await _performDataUpload(); | ||
| 360 | + } | ||
| 361 | + bool hasUploaded = false; | ||
| 362 | + switch (await _healthApi.getHealthDataEverUploaded()) { | ||
| 363 | + case AppSuccess(:final data): | ||
| 364 | + hasUploaded = data.flag ?? false; | ||
| 365 | + if (hasUploaded && data.startTime != null && data.startTime! > 0) { | ||
| 366 | + final firstDayTime = DateUtils.dateOnly( | ||
| 367 | + DateTime.fromMillisecondsSinceEpoch(data.startTime! * 1000), | ||
| 368 | + ); | ||
| 369 | + if (firstDayTime.isBefore(firstTime)) { | ||
| 370 | + firstSelectableDay.value = firstDayTime; | ||
| 371 | + } | ||
| 372 | + } | ||
| 373 | + case AppFailure(): | ||
| 374 | + hasUploaded = false; | ||
| 375 | + } | ||
| 376 | + showHealthDataAuthCardStatus.value = !hasUploaded ? -1 : result.status; | ||
| 377 | + } catch (e) {} | ||
| 378 | + } | ||
| 351 | } | 379 | } |
| 352 | 380 | ||
| 353 | Future<void> checkNotificationCardVisible() async { | 381 | Future<void> checkNotificationCardVisible() async { |
| @@ -388,12 +416,6 @@ class TodayController extends GetMaterialController { | @@ -388,12 +416,6 @@ class TodayController extends GetMaterialController { | ||
| 388 | switch (await _healthApi.getV2StressScore(friendUserId, intDate)) { | 416 | switch (await _healthApi.getV2StressScore(friendUserId, intDate)) { |
| 389 | case AppSuccess(:final data): | 417 | case AppSuccess(:final data): |
| 390 | v2StressScore.value = data; | 418 | v2StressScore.value = data; |
| 391 | - | ||
| 392 | - // var nextInt = Random().nextInt(4); | ||
| 393 | - // v2StressScore.value = V2StressScore( | ||
| 394 | - // state: nextInt, | ||
| 395 | - // comprehensiveScore: 100 - (nextInt * 25), | ||
| 396 | - // ); | ||
| 397 | case AppFailure(): | 419 | case AppFailure(): |
| 398 | v2StressScore.value = null; | 420 | v2StressScore.value = null; |
| 399 | } | 421 | } |
| @@ -408,25 +430,6 @@ class TodayController extends GetMaterialController { | @@ -408,25 +430,6 @@ class TodayController extends GetMaterialController { | ||
| 408 | switch (await _healthApi.getV2HrvTrend(friendUserId, intDate)) { | 430 | switch (await _healthApi.getV2HrvTrend(friendUserId, intDate)) { |
| 409 | case AppSuccess(:final data): | 431 | case AppSuccess(:final data): |
| 410 | hrvChartData.assignAll(data.list ?? []); | 432 | hrvChartData.assignAll(data.list ?? []); |
| 411 | - // hrvChartData.assignAll([ | ||
| 412 | - // // 每小时一条,00:00 ~ 15:00(CST 午夜 = 1782057600) | ||
| 413 | - // V2HrvTrendItem(time: 1782057600, trendHrv: 45, state: 3), // 00:00 | ||
| 414 | - // V2HrvTrendItem(time: 1782061200, trendHrv: 42, state: 3), // 01:00 | ||
| 415 | - // V2HrvTrendItem(time: 1782064800, trendHrv: 38, state: 3), // 02:00 | ||
| 416 | - // V2HrvTrendItem(time: 1782068400, trendHrv: 35, state: 2), // 03:00 | ||
| 417 | - // V2HrvTrendItem(time: 1782072000, trendHrv: 40, state: 3), // 04:00 | ||
| 418 | - // V2HrvTrendItem(time: 1782075600, trendHrv: 55, state: 4), // 05:00 | ||
| 419 | - // V2HrvTrendItem(time: 1782079200, trendHrv: 65, state: 4), // 06:00 | ||
| 420 | - // V2HrvTrendItem(time: 1782082800, trendHrv: 72, state: 4), // 07:00 | ||
| 421 | - // V2HrvTrendItem(time: 1782086400, trendHrv: 68, state: 4), // 08:00 | ||
| 422 | - // V2HrvTrendItem(time: 1782090000, trendHrv: 58, state: 4), // 09:00 | ||
| 423 | - // V2HrvTrendItem(time: 1782093600, trendHrv: 52, state: 3), // 10:00 | ||
| 424 | - // V2HrvTrendItem(time: 1782097200, trendHrv: 48, state: 3), // 11:00 | ||
| 425 | - // V2HrvTrendItem(time: 1782100800, trendHrv: 44, state: 3), // 12:00 | ||
| 426 | - // V2HrvTrendItem(time: 1782104400, trendHrv: 38, state: 2), // 13:00 | ||
| 427 | - // V2HrvTrendItem(time: 1782108000, trendHrv: 42, state: 3), // 14:00 | ||
| 428 | - // V2HrvTrendItem(time: 1782111600, trendHrv: 45, state: 3), // 15:00 | ||
| 429 | - // ]); | ||
| 430 | case AppFailure(): | 433 | case AppFailure(): |
| 431 | hrvChartData.clear(); | 434 | hrvChartData.clear(); |
| 432 | } | 435 | } |
| @@ -434,40 +437,6 @@ class TodayController extends GetMaterialController { | @@ -434,40 +437,6 @@ class TodayController extends GetMaterialController { | ||
| 434 | switch (await _healthApi.getV2RealtimeStress(friendUserId, intDate)) { | 437 | switch (await _healthApi.getV2RealtimeStress(friendUserId, intDate)) { |
| 435 | case AppSuccess(:final data): | 438 | case AppSuccess(:final data): |
| 436 | stressChartData.assignAll(data.list ?? []); | 439 | stressChartData.assignAll(data.list ?? []); |
| 437 | - // stressChartData.assignAll( | ||
| 438 | - // // 每 6 分钟一条,00:00 ~ 15:00(共 150 条) | ||
| 439 | - // // midnight CST = 1782057600,步长 360s | ||
| 440 | - // List.generate(150, (i) { | ||
| 441 | - // const midnight = 1782057600; | ||
| 442 | - // final ts = midnight + i * 360; | ||
| 443 | - // final minutesFromMidnight = i * 6; | ||
| 444 | - // final hour = minutesFromMidnight ~/ 60; | ||
| 445 | - // final minInHour = minutesFromMidnight % 60; | ||
| 446 | - // // 用正弦波模拟真实波动,早高峰(7-9点)压力最高 | ||
| 447 | - // final base = hour < 6 | ||
| 448 | - // ? 25.0 // 深夜低压力 | ||
| 449 | - // : hour < 9 | ||
| 450 | - // ? 55.0 + (hour - 6) * 10.0 // 早高峰爬升 | ||
| 451 | - // : hour < 12 | ||
| 452 | - // ? 75.0 - (hour - 9) * 8.0 // 上午下降 | ||
| 453 | - // : 50.0 - (hour - 12) * 3.0; // 下午缓降 | ||
| 454 | - // // 叠加微小波动(用 i 模拟随机) | ||
| 455 | - // final jitter = (i % 7 - 3) * 2.0 + (minInHour % 3) * 1.5; | ||
| 456 | - // final value = (base + jitter).clamp(10.0, 100.0).round(); | ||
| 457 | - // final int state; | ||
| 458 | - // if (value >= 75) { | ||
| 459 | - // state = 1; | ||
| 460 | - // } else if (value >= 55) { | ||
| 461 | - // state = 2; | ||
| 462 | - // } else if (value >= 35) { | ||
| 463 | - // state = 3; | ||
| 464 | - // } else { | ||
| 465 | - // state = 4; | ||
| 466 | - // } | ||
| 467 | - // return V2RealtimeStressItem(time: ts, value: value, state: state); | ||
| 468 | - // }), | ||
| 469 | - // ); | ||
| 470 | - | ||
| 471 | case AppFailure(): | 440 | case AppFailure(): |
| 472 | stressChartData.clear(); | 441 | stressChartData.clear(); |
| 473 | } | 442 | } |
| @@ -479,12 +448,6 @@ class TodayController extends GetMaterialController { | @@ -479,12 +448,6 @@ class TodayController extends GetMaterialController { | ||
| 479 | v2ActivityTarget.value = null; | 448 | v2ActivityTarget.value = null; |
| 480 | } | 449 | } |
| 481 | } | 450 | } |
| 482 | - // Future.delayed(const Duration(milliseconds: 5000), () { | ||
| 483 | - // firstSelectableDay.value = DateUtils.dateOnly( | ||
| 484 | - // DateTime( | ||
| 485 | - // DateTime.now().year, DateTime.now().month, DateTime.now().day - 1), | ||
| 486 | - // ); | ||
| 487 | - // }); | ||
| 488 | } | 451 | } |
| 489 | 452 | ||
| 490 | void _clearRealTimeData() { | 453 | void _clearRealTimeData() { |
| @@ -588,8 +551,11 @@ class TodayController extends GetMaterialController { | @@ -588,8 +551,11 @@ class TodayController extends GetMaterialController { | ||
| 588 | RxList<FriendItem> friendsList = RxList.empty(); | 551 | RxList<FriendItem> friendsList = RxList.empty(); |
| 589 | int friendsListLimit = 10; | 552 | int friendsListLimit = 10; |
| 590 | 553 | ||
| 591 | - Future<void> toPremiumPage() async { | ||
| 592 | - await Get.toNamed(Routes.PURCHASE); | 554 | + Future<void> toPremiumPage(String channelType) async { |
| 555 | + await Get.toNamed( | ||
| 556 | + Routes.PURCHASE, | ||
| 557 | + arguments: {IntentKeys.channelType: channelType}, | ||
| 558 | + ); | ||
| 593 | final vipResult = await _vipApi.getVipInfo(); | 559 | final vipResult = await _vipApi.getVipInfo(); |
| 594 | if (vipResult case AppSuccess(data: final vip)) { | 560 | if (vipResult case AppSuccess(data: final vip)) { |
| 595 | try { | 561 | try { |
| @@ -688,4 +654,11 @@ class TodayController extends GetMaterialController { | @@ -688,4 +654,11 @@ class TodayController extends GetMaterialController { | ||
| 688 | unawaited(loadDataForDate(selectedDate.value)); | 654 | unawaited(loadDataForDate(selectedDate.value)); |
| 689 | } | 655 | } |
| 690 | } | 656 | } |
| 657 | + | ||
| 658 | + Future<void> refreshTab() async { | ||
| 659 | + checkAddFriendVisible(); | ||
| 660 | + checkHrvAdBannerVisible(); | ||
| 661 | + checkHealthDataAuthCardVisible(); | ||
| 662 | + await loadDataForDate(selectedDate.value); | ||
| 663 | + } | ||
| 691 | } | 664 | } |
| @@ -60,7 +60,11 @@ class TodayTabBody extends StatelessWidget { | @@ -60,7 +60,11 @@ class TodayTabBody extends StatelessWidget { | ||
| 60 | } | 60 | } |
| 61 | return false; | 61 | return false; |
| 62 | }, | 62 | }, |
| 63 | - child: _buildDayScrollView(context, pinnedHeaderHeight), | 63 | + child: RefreshIndicator( |
| 64 | + edgeOffset: pinnedHeaderHeight, | ||
| 65 | + onRefresh: () async => controller.refreshTab(), | ||
| 66 | + child: _buildDayScrollView(context, pinnedHeaderHeight), | ||
| 67 | + ), | ||
| 64 | ), | 68 | ), |
| 65 | Positioned( | 69 | Positioned( |
| 66 | top: 0, | 70 | top: 0, |
| @@ -121,7 +125,9 @@ class TodayTabBody extends StatelessWidget { | @@ -121,7 +125,9 @@ class TodayTabBody extends StatelessWidget { | ||
| 121 | ) { | 125 | ) { |
| 122 | return CustomScrollView( | 126 | return CustomScrollView( |
| 123 | controller: controller.scrollController, | 127 | controller: controller.scrollController, |
| 124 | - physics: const ClampingScrollPhysics(), | 128 | + physics: const AlwaysScrollableScrollPhysics( |
| 129 | + parent: ClampingScrollPhysics(), | ||
| 130 | + ), | ||
| 125 | slivers: [ | 131 | slivers: [ |
| 126 | SliverToBoxAdapter( | 132 | SliverToBoxAdapter( |
| 127 | child: Container( | 133 | child: Container( |
| @@ -322,6 +328,7 @@ class TodayTabBody extends StatelessWidget { | @@ -322,6 +328,7 @@ class TodayTabBody extends StatelessWidget { | ||
| 322 | child: SizedBox( | 328 | child: SizedBox( |
| 323 | height: _weekRowsHeight, | 329 | height: _weekRowsHeight, |
| 324 | child: TableCalendar( | 330 | child: TableCalendar( |
| 331 | + key: ValueKey(controller.firstSelectableDay.value), | ||
| 325 | calendarFormat: CalendarFormat.week, | 332 | calendarFormat: CalendarFormat.week, |
| 326 | headerVisible: false, | 333 | headerVisible: false, |
| 327 | daysOfWeekHeight: _weekRowsHeight / 2, | 334 | daysOfWeekHeight: _weekRowsHeight / 2, |
| @@ -375,7 +382,7 @@ class TodayTabBody extends StatelessWidget { | @@ -375,7 +382,7 @@ class TodayTabBody extends StatelessWidget { | ||
| 375 | GestureDetector( | 382 | GestureDetector( |
| 376 | onTap: () async { | 383 | onTap: () async { |
| 377 | if (!Get.find<UserStateService>().isVip) { | 384 | if (!Get.find<UserStateService>().isVip) { |
| 378 | - controller.toPremiumPage(); | 385 | + controller.toPremiumPage('今日-查看非当日数据'); |
| 379 | return; | 386 | return; |
| 380 | } | 387 | } |
| 381 | final picked = await showReportDatePickerSheet( | 388 | final picked = await showReportDatePickerSheet( |
| @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; | @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; | ||
| 2 | import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; | 2 | import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; |
| 3 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 3 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| 4 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; | 4 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; |
| 5 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 5 | import 'package:flutter/material.dart'; | 6 | import 'package:flutter/material.dart'; |
| 6 | import 'package:get/get.dart'; | 7 | import 'package:get/get.dart'; |
| 7 | 8 | ||
| @@ -33,7 +34,7 @@ class FriendSelectBottomSheet extends StatelessWidget { | @@ -33,7 +34,7 @@ class FriendSelectBottomSheet extends StatelessWidget { | ||
| 33 | children: [ | 34 | children: [ |
| 34 | Center( | 35 | Center( |
| 35 | child: Text( | 36 | child: Text( |
| 36 | - '选择好友', | 37 | + l10n.friendsSelect, |
| 37 | style: TextStyle( | 38 | style: TextStyle( |
| 38 | color: context.colors.textPrimary, | 39 | color: context.colors.textPrimary, |
| 39 | fontSize: 16, | 40 | fontSize: 16, |
| @@ -79,6 +80,7 @@ class FriendSelectBottomSheet extends StatelessWidget { | @@ -79,6 +80,7 @@ class FriendSelectBottomSheet extends StatelessWidget { | ||
| 79 | friend: friend, | 80 | friend: friend, |
| 80 | isSelected: isSelected, | 81 | isSelected: isSelected, |
| 81 | onTap: () { | 82 | onTap: () { |
| 83 | + Get.back(); | ||
| 82 | controller.selectFriend(friend); | 84 | controller.selectFriend(friend); |
| 83 | }, | 85 | }, |
| 84 | ); | 86 | ); |
| @@ -151,7 +151,7 @@ class PremiumCard extends StatelessWidget { | @@ -151,7 +151,7 @@ class PremiumCard extends StatelessWidget { | ||
| 151 | mainAxisAlignment: MainAxisAlignment.spaceBetween, | 151 | mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 152 | children: [ | 152 | children: [ |
| 153 | GestureDetector( | 153 | GestureDetector( |
| 154 | - onTap: () => controller.toPremiumPage(), | 154 | + onTap: () => controller.toPremiumPage('今日-会员广告'), |
| 155 | behavior: HitTestBehavior.opaque, | 155 | behavior: HitTestBehavior.opaque, |
| 156 | child: Text( | 156 | child: Text( |
| 157 | context.l10n.allPlans, | 157 | context.l10n.allPlans, |
| @@ -256,6 +256,7 @@ class _ContactText extends StatelessWidget { | @@ -256,6 +256,7 @@ class _ContactText extends StatelessWidget { | ||
| 256 | child: Text( | 256 | child: Text( |
| 257 | l10n.todayFaqContactAction, | 257 | l10n.todayFaqContactAction, |
| 258 | style: TextStyle( | 258 | style: TextStyle( |
| 259 | + fontSize: 12, | ||
| 259 | color: context.colors.primary, | 260 | color: context.colors.primary, |
| 260 | fontWeight: FontWeight.w600, | 261 | fontWeight: FontWeight.w600, |
| 261 | decoration: TextDecoration.underline, | 262 | decoration: TextDecoration.underline, |
| @@ -67,7 +67,7 @@ class TodayHealthDataAuthCard extends StatelessWidget { | @@ -67,7 +67,7 @@ class TodayHealthDataAuthCard extends StatelessWidget { | ||
| 67 | style: TextStyle( | 67 | style: TextStyle( |
| 68 | color: colors.primary, | 68 | color: colors.primary, |
| 69 | fontSize: 12, | 69 | fontSize: 12, |
| 70 | - fontWeight: FontWeight.w500, | 70 | + fontWeight: FontWeight.w600, |
| 71 | height: 1.2, | 71 | height: 1.2, |
| 72 | ), | 72 | ), |
| 73 | ), | 73 | ), |
| @@ -238,7 +238,7 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -238,7 +238,7 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 238 | DateTime selectedDate, | 238 | DateTime selectedDate, |
| 239 | bool isVip) { | 239 | bool isVip) { |
| 240 | void onTapPurchase() { | 240 | void onTapPurchase() { |
| 241 | - controller.toPremiumPage(); | 241 | + controller.toPremiumPage('今日-实时压力-立即解锁'); |
| 242 | } | 242 | } |
| 243 | 243 | ||
| 244 | // 槽时间秒 → state 映射(与 _buildStressBarGroups 一致:取 value 最大那条 of state) | 244 | // 槽时间秒 → state 映射(与 _buildStressBarGroups 一致:取 value 最大那条 of state) |
| @@ -384,8 +384,17 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -384,8 +384,17 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 384 | List<TextSpan>? _getTooltip(BarChartRodData rod, int x, int? state) { | 384 | List<TextSpan>? _getTooltip(BarChartRodData rod, int x, int? state) { |
| 385 | return [ | 385 | return [ |
| 386 | TextSpan( | 386 | TextSpan( |
| 387 | - text: '○ ${_getStatusByState(state)} ${_formatNumber(rod.toY)}%', | 387 | + text: '○ ', |
| 388 | style: TextStyle( | 388 | style: TextStyle( |
| 389 | + fontSize: 14, | ||
| 390 | + color: _getColorByState(state), | ||
| 391 | + fontWeight: FontWeight.bold, | ||
| 392 | + ), | ||
| 393 | + ), | ||
| 394 | + TextSpan( | ||
| 395 | + text: '${_getStatusByState(state)} ${_formatNumber(rod.toY)}%', | ||
| 396 | + style: TextStyle( | ||
| 397 | + fontSize: 18, | ||
| 389 | color: _getColorByState(state), | 398 | color: _getColorByState(state), |
| 390 | fontWeight: FontWeight.bold, | 399 | fontWeight: FontWeight.bold, |
| 391 | ), | 400 | ), |
| @@ -398,9 +407,18 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -398,9 +407,18 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 398 | ), | 407 | ), |
| 399 | ), | 408 | ), |
| 400 | TextSpan( | 409 | TextSpan( |
| 401 | - text: '♥ ${l10n.pressure} ${_formatHour(x.toDouble())}', | 410 | + text: '♥ ', |
| 402 | style: const TextStyle( | 411 | style: const TextStyle( |
| 403 | color: Color(0xFF78787D), | 412 | color: Color(0xFF78787D), |
| 413 | + fontSize: 14, | ||
| 414 | + fontWeight: FontWeight.bold, | ||
| 415 | + ), | ||
| 416 | + ), | ||
| 417 | + TextSpan( | ||
| 418 | + text: '${l10n.realTimePressure} · ${_formatHour(x.toDouble())}', | ||
| 419 | + style: const TextStyle( | ||
| 420 | + color: Color(0xFF78787D), | ||
| 421 | + fontSize: 14, | ||
| 404 | fontWeight: FontWeight.bold, | 422 | fontWeight: FontWeight.bold, |
| 405 | ), | 423 | ), |
| 406 | ), | 424 | ), |
| @@ -410,7 +428,7 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -410,7 +428,7 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 410 | List<TextSpan>? _getTooltipChildren(LineBarSpot flSpot, int? state) { | 428 | List<TextSpan>? _getTooltipChildren(LineBarSpot flSpot, int? state) { |
| 411 | return [ | 429 | return [ |
| 412 | TextSpan( | 430 | TextSpan( |
| 413 | - text: '○ ${_getStatusByState(state)}', | 431 | + text: '○ ', |
| 414 | style: TextStyle( | 432 | style: TextStyle( |
| 415 | color: _getColorByState(state), | 433 | color: _getColorByState(state), |
| 416 | // fontWeight: FontWeight.bold, | 434 | // fontWeight: FontWeight.bold, |
| @@ -418,6 +436,14 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -418,6 +436,14 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 418 | ), | 436 | ), |
| 419 | ), | 437 | ), |
| 420 | TextSpan( | 438 | TextSpan( |
| 439 | + text: _getStatusByState(state), | ||
| 440 | + style: TextStyle( | ||
| 441 | + color: _getColorByState(state), | ||
| 442 | + // fontWeight: FontWeight.bold, | ||
| 443 | + fontSize: 18, | ||
| 444 | + ), | ||
| 445 | + ), | ||
| 446 | + TextSpan( | ||
| 421 | text: '\n', | 447 | text: '\n', |
| 422 | style: TextStyle( | 448 | style: TextStyle( |
| 423 | color: Colors.white, | 449 | color: Colors.white, |
| @@ -425,7 +451,15 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -425,7 +451,15 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 425 | ), | 451 | ), |
| 426 | ), | 452 | ), |
| 427 | TextSpan( | 453 | TextSpan( |
| 428 | - text: '♥ HRV ${_formatNumber(flSpot.y)}ms · ${_formatHour(flSpot.x)}', | 454 | + text: '♥ ', |
| 455 | + style: const TextStyle( | ||
| 456 | + color: Color(0xFF78787D), | ||
| 457 | + fontSize: 14, | ||
| 458 | + fontWeight: FontWeight.bold, | ||
| 459 | + ), | ||
| 460 | + ), | ||
| 461 | + TextSpan( | ||
| 462 | + text: 'HRV ${_formatNumber(flSpot.y)}ms · ${_formatHour(flSpot.x)}', | ||
| 429 | style: const TextStyle( | 463 | style: const TextStyle( |
| 430 | color: Color(0xFF78787D), | 464 | color: Color(0xFF78787D), |
| 431 | fontWeight: FontWeight.bold, | 465 | fontWeight: FontWeight.bold, |
| @@ -574,6 +608,7 @@ class _HrvLineChartState extends State<_HrvLineChart> { | @@ -574,6 +608,7 @@ class _HrvLineChartState extends State<_HrvLineChart> { | ||
| 574 | ], | 608 | ], |
| 575 | lineTouchData: LineTouchData( | 609 | lineTouchData: LineTouchData( |
| 576 | handleBuiltInTouches: false, | 610 | handleBuiltInTouches: false, |
| 611 | + touchSpotThreshold: 20.0, | ||
| 577 | touchCallback: | 612 | touchCallback: |
| 578 | (FlTouchEvent event, LineTouchResponse? touchResponse) { | 613 | (FlTouchEvent event, LineTouchResponse? touchResponse) { |
| 579 | if (event is FlPanStartEvent || | 614 | if (event is FlPanStartEvent || |
| @@ -588,6 +623,10 @@ class _HrvLineChartState extends State<_HrvLineChart> { | @@ -588,6 +623,10 @@ class _HrvLineChartState extends State<_HrvLineChart> { | ||
| 588 | setState(() { | 623 | setState(() { |
| 589 | _hrvShowingSpots = touchResponse.lineBarSpots!; | 624 | _hrvShowingSpots = touchResponse.lineBarSpots!; |
| 590 | }); | 625 | }); |
| 626 | + } else { | ||
| 627 | + setState(() { | ||
| 628 | + _hrvShowingSpots = []; | ||
| 629 | + }); | ||
| 591 | } | 630 | } |
| 592 | } else if (event is FlPanEndEvent || | 631 | } else if (event is FlPanEndEvent || |
| 593 | event is FlTapUpEvent || | 632 | event is FlTapUpEvent || |
| @@ -700,11 +739,11 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -700,11 +739,11 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 700 | Widget build(BuildContext context) { | 739 | Widget build(BuildContext context) { |
| 701 | return LayoutBuilder( | 740 | return LayoutBuilder( |
| 702 | builder: (context, constraints) { | 741 | builder: (context, constraints) { |
| 703 | - // 右轴 reservedSize=22,剩余宽度平分给所有槽 | ||
| 704 | - const rightAxisWidth = 22.0; | 742 | + // 右轴 reservedSize=22,不占用空间设为 0.0 |
| 743 | + const rightAxisWidth = 0.0; | ||
| 705 | const slotSec = 6 * 60; | 744 | const slotSec = 6 * 60; |
| 706 | final slotCount = (widget.maxSeconds / slotSec).ceil().clamp(1, 9999); | 745 | final slotCount = (widget.maxSeconds / slotSec).ceil().clamp(1, 9999); |
| 707 | - const double groupsSpace = 0; // 设置间隔为 1 像素 | 746 | + const double groupsSpace = 0.25; // 设置间隔为 1 像素 |
| 708 | final barWidth = (constraints.maxWidth - | 747 | final barWidth = (constraints.maxWidth - |
| 709 | rightAxisWidth - | 748 | rightAxisWidth - |
| 710 | (groupsSpace * (slotCount - 1))) / | 749 | (groupsSpace * (slotCount - 1))) / |
| @@ -822,11 +861,16 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -822,11 +861,16 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 822 | } | 861 | } |
| 823 | } | 862 | } |
| 824 | 863 | ||
| 864 | + final chartHeight = constraints.maxHeight; | ||
| 865 | + const gridTop = 18.0; | ||
| 866 | + final gridBottom = chartHeight - 20.0; | ||
| 867 | + final gridHeight = gridBottom - gridTop; | ||
| 868 | + | ||
| 825 | return Stack( | 869 | return Stack( |
| 826 | clipBehavior: Clip.none, | 870 | clipBehavior: Clip.none, |
| 827 | children: [ | 871 | children: [ |
| 828 | - Padding( | ||
| 829 | - padding: const EdgeInsets.only(top: 6), | 872 | + Container( |
| 873 | + margin: const EdgeInsets.only(top: gridTop), | ||
| 830 | child: BarChart( | 874 | child: BarChart( |
| 831 | BarChartData( | 875 | BarChartData( |
| 832 | minY: 0, | 876 | minY: 0, |
| @@ -868,28 +912,12 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -868,28 +912,12 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 868 | sideTitles: SideTitles(showTitles: false)), | 912 | sideTitles: SideTitles(showTitles: false)), |
| 869 | leftTitles: const AxisTitles( | 913 | leftTitles: const AxisTitles( |
| 870 | sideTitles: SideTitles(showTitles: false)), | 914 | sideTitles: SideTitles(showTitles: false)), |
| 871 | - rightTitles: AxisTitles( | ||
| 872 | - sideTitles: SideTitles( | ||
| 873 | - showTitles: true, | ||
| 874 | - interval: 50, | ||
| 875 | - reservedSize: 22, | ||
| 876 | - getTitlesWidget: (val, _) { | ||
| 877 | - if (val < 0 || val > 100) { | ||
| 878 | - return const SizedBox.shrink(); | ||
| 879 | - } | ||
| 880 | - return Text( | ||
| 881 | - '${val.toInt()}', | ||
| 882 | - style: TextStyle( | ||
| 883 | - fontSize: 10, | ||
| 884 | - color: context.colors.textTertiary), | ||
| 885 | - ); | ||
| 886 | - }, | ||
| 887 | - ), | ||
| 888 | - ), | 915 | + rightTitles: const AxisTitles( |
| 916 | + sideTitles: SideTitles(showTitles: false)), | ||
| 889 | bottomTitles: AxisTitles( | 917 | bottomTitles: AxisTitles( |
| 890 | sideTitles: SideTitles( | 918 | sideTitles: SideTitles( |
| 891 | showTitles: true, | 919 | showTitles: true, |
| 892 | - reservedSize: 22, | 920 | + reservedSize: 20, |
| 893 | interval: 6 * 3600, | 921 | interval: 6 * 3600, |
| 894 | getTitlesWidget: (val, meta) { | 922 | getTitlesWidget: (val, meta) { |
| 895 | final label = widget.timeLabel(val); | 923 | final label = widget.timeLabel(val); |
| @@ -914,6 +942,8 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -914,6 +942,8 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 914 | ), | 942 | ), |
| 915 | barTouchData: BarTouchData( | 943 | barTouchData: BarTouchData( |
| 916 | handleBuiltInTouches: false, | 944 | handleBuiltInTouches: false, |
| 945 | + touchExtraThreshold: const EdgeInsets.symmetric( | ||
| 946 | + horizontal: 16, vertical: 40), | ||
| 917 | touchCallback: | 947 | touchCallback: |
| 918 | (FlTouchEvent event, BarTouchResponse? touchResponse) { | 948 | (FlTouchEvent event, BarTouchResponse? touchResponse) { |
| 919 | if (event is FlPanStartEvent || | 949 | if (event is FlPanStartEvent || |
| @@ -928,6 +958,10 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -928,6 +958,10 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 928 | _stressTouchedGroupIndex = | 958 | _stressTouchedGroupIndex = |
| 929 | touchResponse.spot!.touchedBarGroupIndex; | 959 | touchResponse.spot!.touchedBarGroupIndex; |
| 930 | }); | 960 | }); |
| 961 | + } else { | ||
| 962 | + setState(() { | ||
| 963 | + _stressTouchedGroupIndex = null; | ||
| 964 | + }); | ||
| 931 | } | 965 | } |
| 932 | } else if (event is FlPanEndEvent || | 966 | } else if (event is FlPanEndEvent || |
| 933 | event is FlTapUpEvent || | 967 | event is FlTapUpEvent || |
| @@ -978,6 +1012,34 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -978,6 +1012,34 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 978 | ), | 1012 | ), |
| 979 | ), | 1013 | ), |
| 980 | ...sleepWidgets, | 1014 | ...sleepWidgets, |
| 1015 | + // 手绘 100、50、0 标签在对应的横线上方 | ||
| 1016 | + Positioned( | ||
| 1017 | + right: 0, | ||
| 1018 | + top: gridTop - 12, | ||
| 1019 | + child: Text( | ||
| 1020 | + '100', | ||
| 1021 | + style: | ||
| 1022 | + TextStyle(fontSize: 10, color: context.colors.textTertiary), | ||
| 1023 | + ), | ||
| 1024 | + ), | ||
| 1025 | + Positioned( | ||
| 1026 | + right: 0, | ||
| 1027 | + top: gridTop + gridHeight * 0.5 - 12, | ||
| 1028 | + child: Text( | ||
| 1029 | + '50', | ||
| 1030 | + style: | ||
| 1031 | + TextStyle(fontSize: 10, color: context.colors.textTertiary), | ||
| 1032 | + ), | ||
| 1033 | + ), | ||
| 1034 | + Positioned( | ||
| 1035 | + right: 0, | ||
| 1036 | + top: gridBottom - 12, | ||
| 1037 | + child: Text( | ||
| 1038 | + '0', | ||
| 1039 | + style: | ||
| 1040 | + TextStyle(fontSize: 10, color: context.colors.textTertiary), | ||
| 1041 | + ), | ||
| 1042 | + ), | ||
| 981 | ], | 1043 | ], |
| 982 | ); | 1044 | ); |
| 983 | }, | 1045 | }, |
| @@ -1017,7 +1079,7 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -1017,7 +1079,7 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 1017 | color: entry != null | 1079 | color: entry != null |
| 1018 | ? widget.getColorByState(entry.state) | 1080 | ? widget.getColorByState(entry.state) |
| 1019 | : Colors.transparent, | 1081 | : Colors.transparent, |
| 1020 | - borderRadius: BorderRadius.zero, | 1082 | + borderRadius: BorderRadius.circular(barWidth / 2), |
| 1021 | ), | 1083 | ), |
| 1022 | ], | 1084 | ], |
| 1023 | )); | 1085 | )); |
| @@ -24,7 +24,7 @@ class TodayHrvNumberCard extends StatelessWidget { | @@ -24,7 +24,7 @@ class TodayHrvNumberCard extends StatelessWidget { | ||
| 24 | controller.selectedDate.value, controller.lastSelectableDay); | 24 | controller.selectedDate.value, controller.lastSelectableDay); |
| 25 | 25 | ||
| 26 | var otherHrv = (sameDay | 26 | var otherHrv = (sameDay |
| 27 | - ? context.l10n.todaySHrvTrend | 27 | + ? context.l10n.todaySAverageHrv |
| 28 | : context.l10n.averageHrvForTheDay); | 28 | : context.l10n.averageHrvForTheDay); |
| 29 | return Row( | 29 | return Row( |
| 30 | children: [ | 30 | children: [ |
| @@ -53,7 +53,6 @@ class TodaySleepCard extends StatelessWidget { | @@ -53,7 +53,6 @@ class TodaySleepCard extends StatelessWidget { | ||
| 53 | color: context.colors.textPrimary, | 53 | color: context.colors.textPrimary, |
| 54 | fontSize: 16, | 54 | fontSize: 16, |
| 55 | fontWeight: FontWeight.w600, | 55 | fontWeight: FontWeight.w600, |
| 56 | - height: 19 / 16, | ||
| 57 | ), | 56 | ), |
| 58 | children: [ | 57 | children: [ |
| 59 | if (sleepHours > 0) ...[ | 58 | if (sleepHours > 0) ...[ |
| @@ -87,7 +86,6 @@ class TodaySleepCard extends StatelessWidget { | @@ -87,7 +86,6 @@ class TodaySleepCard extends StatelessWidget { | ||
| 87 | color: context.colors.textPrimary, | 86 | color: context.colors.textPrimary, |
| 88 | fontSize: 16, | 87 | fontSize: 16, |
| 89 | fontWeight: FontWeight.w600, | 88 | fontWeight: FontWeight.w600, |
| 90 | - height: 19 / 16, | ||
| 91 | ), | 89 | ), |
| 92 | )), | 90 | )), |
| 93 | _MetricBlock( | 91 | _MetricBlock( |
| @@ -99,7 +97,6 @@ class TodaySleepCard extends StatelessWidget { | @@ -99,7 +97,6 @@ class TodaySleepCard extends StatelessWidget { | ||
| 99 | color: sleepStateColor, | 97 | color: sleepStateColor, |
| 100 | fontSize: 16, | 98 | fontSize: 16, |
| 101 | fontWeight: FontWeight.w600, | 99 | fontWeight: FontWeight.w600, |
| 102 | - height: 19 / 16, | ||
| 103 | ), | 100 | ), |
| 104 | ), | 101 | ), |
| 105 | ), | 102 | ), |
| @@ -113,7 +110,6 @@ class TodaySleepCard extends StatelessWidget { | @@ -113,7 +110,6 @@ class TodaySleepCard extends StatelessWidget { | ||
| 113 | color: context.colors.textPrimary, | 110 | color: context.colors.textPrimary, |
| 114 | fontSize: 16, | 111 | fontSize: 16, |
| 115 | fontWeight: FontWeight.w600, | 112 | fontWeight: FontWeight.w600, |
| 116 | - height: 19 / 16, | ||
| 117 | ), | 113 | ), |
| 118 | children: [ | 114 | children: [ |
| 119 | TextSpan(text: sleepAverageHeartRate), | 115 | TextSpan(text: sleepAverageHeartRate), |
| @@ -134,7 +130,6 @@ class TodaySleepCard extends StatelessWidget { | @@ -134,7 +130,6 @@ class TodaySleepCard extends StatelessWidget { | ||
| 134 | color: context.colors.textPrimary, | 130 | color: context.colors.textPrimary, |
| 135 | fontSize: 16, | 131 | fontSize: 16, |
| 136 | fontWeight: FontWeight.w600, | 132 | fontWeight: FontWeight.w600, |
| 137 | - height: 19 / 16, | ||
| 138 | ), | 133 | ), |
| 139 | ), | 134 | ), |
| 140 | ), | 135 | ), |
| @@ -196,7 +191,6 @@ class TodayActivityCard extends StatelessWidget { | @@ -196,7 +191,6 @@ class TodayActivityCard extends StatelessWidget { | ||
| 196 | color: context.colors.textPrimary, | 191 | color: context.colors.textPrimary, |
| 197 | fontSize: 16, | 192 | fontSize: 16, |
| 198 | fontWeight: FontWeight.w600, | 193 | fontWeight: FontWeight.w600, |
| 199 | - height: 19 / 16, | ||
| 200 | ), | 194 | ), |
| 201 | children: [ | 195 | children: [ |
| 202 | if (exHours > 0) ...[ | 196 | if (exHours > 0) ...[ |
| @@ -231,7 +225,6 @@ class TodayActivityCard extends StatelessWidget { | @@ -231,7 +225,6 @@ class TodayActivityCard extends StatelessWidget { | ||
| 231 | color: context.colors.textPrimary, | 225 | color: context.colors.textPrimary, |
| 232 | fontSize: 16, | 226 | fontSize: 16, |
| 233 | fontWeight: FontWeight.w600, | 227 | fontWeight: FontWeight.w600, |
| 234 | - height: 19 / 16, | ||
| 235 | ), | 228 | ), |
| 236 | children: [ | 229 | children: [ |
| 237 | const TextSpan(text: '-'), | 230 | const TextSpan(text: '-'), |
| @@ -249,7 +242,6 @@ class TodayActivityCard extends StatelessWidget { | @@ -249,7 +242,6 @@ class TodayActivityCard extends StatelessWidget { | ||
| 249 | color: context.colors.textPrimary, | 242 | color: context.colors.textPrimary, |
| 250 | fontSize: 16, | 243 | fontSize: 16, |
| 251 | fontWeight: FontWeight.w600, | 244 | fontWeight: FontWeight.w600, |
| 252 | - height: 19 / 16, | ||
| 253 | ), | 245 | ), |
| 254 | children: [ | 246 | children: [ |
| 255 | TextSpan(text: '$stand'), | 247 | TextSpan(text: '$stand'), |
| @@ -271,7 +263,6 @@ class TodayActivityCard extends StatelessWidget { | @@ -271,7 +263,6 @@ class TodayActivityCard extends StatelessWidget { | ||
| 271 | color: context.colors.textPrimary, | 263 | color: context.colors.textPrimary, |
| 272 | fontSize: 16, | 264 | fontSize: 16, |
| 273 | fontWeight: FontWeight.w600, | 265 | fontWeight: FontWeight.w600, |
| 274 | - height: 19 / 16, | ||
| 275 | ), | 266 | ), |
| 276 | children: [ | 267 | children: [ |
| 277 | const TextSpan(text: '-'), | 268 | const TextSpan(text: '-'), |
| @@ -306,7 +297,6 @@ class TodayActivityCard extends StatelessWidget { | @@ -306,7 +297,6 @@ class TodayActivityCard extends StatelessWidget { | ||
| 306 | color: context.colors.textPrimary, | 297 | color: context.colors.textPrimary, |
| 307 | fontSize: 16, | 298 | fontSize: 16, |
| 308 | fontWeight: FontWeight.w600, | 299 | fontWeight: FontWeight.w600, |
| 309 | - height: 19 / 16, | ||
| 310 | ), | 300 | ), |
| 311 | children: [ | 301 | children: [ |
| 312 | TextSpan(text: activityStr), | 302 | TextSpan(text: activityStr), |
| @@ -427,7 +417,6 @@ class _TodaySummaryCard extends StatelessWidget { | @@ -427,7 +417,6 @@ class _TodaySummaryCard extends StatelessWidget { | ||
| 427 | color: colors.textPrimary, | 417 | color: colors.textPrimary, |
| 428 | fontSize: 14, | 418 | fontSize: 14, |
| 429 | fontWeight: FontWeight.w500, | 419 | fontWeight: FontWeight.w500, |
| 430 | - height: 20 / 14, | ||
| 431 | ), | 420 | ), |
| 432 | ), | 421 | ), |
| 433 | const Spacer(), | 422 | const Spacer(), |
| @@ -437,7 +426,6 @@ class _TodaySummaryCard extends StatelessWidget { | @@ -437,7 +426,6 @@ class _TodaySummaryCard extends StatelessWidget { | ||
| 437 | color: colors.textSecondary, | 426 | color: colors.textSecondary, |
| 438 | fontSize: 12, | 427 | fontSize: 12, |
| 439 | fontWeight: FontWeight.w400, | 428 | fontWeight: FontWeight.w400, |
| 440 | - height: 17 / 12, | ||
| 441 | ), | 429 | ), |
| 442 | ), | 430 | ), |
| 443 | Image.asset( | 431 | Image.asset( |
| @@ -495,7 +483,6 @@ class _MetricBlock extends StatelessWidget { | @@ -495,7 +483,6 @@ class _MetricBlock extends StatelessWidget { | ||
| 495 | color: labelColor, | 483 | color: labelColor, |
| 496 | fontSize: 12, | 484 | fontSize: 12, |
| 497 | fontWeight: FontWeight.w500, | 485 | fontWeight: FontWeight.w500, |
| 498 | - height: 17 / 12, | ||
| 499 | ), | 486 | ), |
| 500 | ), | 487 | ), |
| 501 | const SizedBox(height: 5), | 488 | const SizedBox(height: 5), |
| @@ -351,11 +351,12 @@ class HealthApi { | @@ -351,11 +351,12 @@ class HealthApi { | ||
| 351 | ); | 351 | ); |
| 352 | } | 352 | } |
| 353 | 353 | ||
| 354 | - Future<AppResult<bool>> getHealthDataEverUploaded() { | 354 | + Future<AppResult<V2HealthEverUploaded>> getHealthDataEverUploaded() { |
| 355 | return safeCall( | 355 | return safeCall( |
| 356 | call: () async { | 356 | call: () async { |
| 357 | final response = await _dioClient.dio.get(ApiPaths.weatherUploadedData); | 357 | final response = await _dioClient.dio.get(ApiPaths.weatherUploadedData); |
| 358 | - return response.data['flag']; | 358 | + return V2HealthEverUploaded.fromJson( |
| 359 | + response.data as Map<String, dynamic>); | ||
| 359 | }, | 360 | }, |
| 360 | errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy, | 361 | errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy, |
| 361 | ); | 362 | ); |
| @@ -55,9 +55,9 @@ class V2LatestHrvData { | @@ -55,9 +55,9 @@ class V2LatestHrvData { | ||
| 55 | case 2: | 55 | case 2: |
| 56 | return context.colors.chartOrange; | 56 | return context.colors.chartOrange; |
| 57 | case 3: | 57 | case 3: |
| 58 | - return context.colors.chartGreen; | ||
| 59 | - case 4: | ||
| 60 | return context.colors.chartBlue; | 58 | return context.colors.chartBlue; |
| 59 | + case 4: | ||
| 60 | + return context.colors.chartGreen; | ||
| 61 | } | 61 | } |
| 62 | return context.colors.chartGreen; | 62 | return context.colors.chartGreen; |
| 63 | } | 63 | } |
| @@ -437,3 +437,27 @@ class V2ActivityTarget { | @@ -437,3 +437,27 @@ class V2ActivityTarget { | ||
| 437 | return val; | 437 | return val; |
| 438 | } | 438 | } |
| 439 | } | 439 | } |
| 440 | + | ||
| 441 | +class V2HealthEverUploaded { | ||
| 442 | + const V2HealthEverUploaded({ | ||
| 443 | + this.flag, | ||
| 444 | + this.startTime, | ||
| 445 | + }); | ||
| 446 | + | ||
| 447 | + final bool? flag; | ||
| 448 | + final int? startTime; | ||
| 449 | + | ||
| 450 | + factory V2HealthEverUploaded.fromJson(Map<String, dynamic> json) { | ||
| 451 | + return V2HealthEverUploaded( | ||
| 452 | + flag: json['flag'] as bool?, | ||
| 453 | + startTime: _parseInt(json['start_time']), | ||
| 454 | + ); | ||
| 455 | + } | ||
| 456 | + | ||
| 457 | + Map<String, dynamic> toJson() { | ||
| 458 | + return { | ||
| 459 | + 'flag': flag, | ||
| 460 | + 'start_time': startTime, | ||
| 461 | + }; | ||
| 462 | + } | ||
| 463 | +} |
| @@ -562,5 +562,6 @@ | @@ -562,5 +562,6 @@ | ||
| 562 | "freeRedemptionOffer": "Free Redemption Offer", | 562 | "freeRedemptionOffer": "Free Redemption Offer", |
| 563 | "cellPhoneNumber": "Cell phone number", | 563 | "cellPhoneNumber": "Cell phone number", |
| 564 | "todayOnWeeklyCalendar": "today", | 564 | "todayOnWeeklyCalendar": "today", |
| 565 | - "hrvTrendForThatDay": "HRV Trend for That Day" | 565 | + "hrvTrendForThatDay": "HRV Trend for That Day", |
| 566 | + "todaySAverageHrv": "Today's Average HRV" | ||
| 566 | } | 567 | } |
| @@ -850,5 +850,6 @@ | @@ -850,5 +850,6 @@ | ||
| 850 | "freeRedemptionOffer": "免费兑换优惠", | 850 | "freeRedemptionOffer": "免费兑换优惠", |
| 851 | "cellPhoneNumber": "手机号", | 851 | "cellPhoneNumber": "手机号", |
| 852 | "todayOnWeeklyCalendar": "今", | 852 | "todayOnWeeklyCalendar": "今", |
| 853 | - "hrvTrendForThatDay": "该日HRV趋势" | 853 | + "hrvTrendForThatDay": "该日HRV趋势", |
| 854 | + "todaySAverageHrv": "今日平均HRV" | ||
| 854 | } | 855 | } |
| @@ -3332,6 +3332,12 @@ abstract class AppLocalizations { | @@ -3332,6 +3332,12 @@ abstract class AppLocalizations { | ||
| 3332 | /// In zh, this message translates to: | 3332 | /// In zh, this message translates to: |
| 3333 | /// **'该日HRV趋势'** | 3333 | /// **'该日HRV趋势'** |
| 3334 | String get hrvTrendForThatDay; | 3334 | String get hrvTrendForThatDay; |
| 3335 | + | ||
| 3336 | + /// No description provided for @todaySAverageHrv. | ||
| 3337 | + /// | ||
| 3338 | + /// In zh, this message translates to: | ||
| 3339 | + /// **'今日平均HRV'** | ||
| 3340 | + String get todaySAverageHrv; | ||
| 3335 | } | 3341 | } |
| 3336 | 3342 | ||
| 3337 | class _AppLocalizationsDelegate | 3343 | class _AppLocalizationsDelegate |
| @@ -1856,4 +1856,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1856,4 +1856,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1856 | 1856 | ||
| 1857 | @override | 1857 | @override |
| 1858 | String get hrvTrendForThatDay => 'HRV Trend for That Day'; | 1858 | String get hrvTrendForThatDay => 'HRV Trend for That Day'; |
| 1859 | + | ||
| 1860 | + @override | ||
| 1861 | + String get todaySAverageHrv => 'Today\'s Average HRV'; | ||
| 1859 | } | 1862 | } |
| @@ -1757,4 +1757,7 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1757,4 +1757,7 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1757 | 1757 | ||
| 1758 | @override | 1758 | @override |
| 1759 | String get hrvTrendForThatDay => '该日HRV趋势'; | 1759 | String get hrvTrendForThatDay => '该日HRV趋势'; |
| 1760 | + | ||
| 1761 | + @override | ||
| 1762 | + String get todaySAverageHrv => '今日平均HRV'; | ||
| 1760 | } | 1763 | } |
-
Please register or login to post a comment