Showing
23 changed files
with
503 additions
and
90 deletions
| @@ -3,7 +3,7 @@ | @@ -3,7 +3,7 @@ | ||
| 3 | archiveVersion = 1; | 3 | archiveVersion = 1; |
| 4 | classes = { | 4 | classes = { |
| 5 | }; | 5 | }; |
| 6 | - objectVersion = 77; | 6 | + objectVersion = 54; |
| 7 | objects = { | 7 | objects = { |
| 8 | 8 | ||
| 9 | /* Begin PBXBuildFile section */ | 9 | /* Begin PBXBuildFile section */ |
| @@ -283,6 +283,7 @@ | @@ -283,6 +283,7 @@ | ||
| 283 | 66FBE5882FDA4F0F00F515B4 /* Frameworks */, | 283 | 66FBE5882FDA4F0F00F515B4 /* Frameworks */, |
| 284 | 66FBE5892FDA4F0F00F515B4 /* Resources */, | 284 | 66FBE5892FDA4F0F00F515B4 /* Resources */, |
| 285 | 66FBE5BE2FDA518D00F515B4 /* Embed Foundation Extensions */, | 285 | 66FBE5BE2FDA518D00F515B4 /* Embed Foundation Extensions */, |
| 286 | + 1F69C09D185F42507815E3C2 /* [CP] Embed Pods Frameworks */, | ||
| 286 | ); | 287 | ); |
| 287 | buildRules = ( | 288 | buildRules = ( |
| 288 | ); | 289 | ); |
| @@ -364,6 +365,23 @@ | @@ -364,6 +365,23 @@ | ||
| 364 | /* End PBXResourcesBuildPhase section */ | 365 | /* End PBXResourcesBuildPhase section */ |
| 365 | 366 | ||
| 366 | /* Begin PBXShellScriptBuildPhase section */ | 367 | /* Begin PBXShellScriptBuildPhase section */ |
| 368 | + 1F69C09D185F42507815E3C2 /* [CP] Embed Pods Frameworks */ = { | ||
| 369 | + isa = PBXShellScriptBuildPhase; | ||
| 370 | + buildActionMask = 2147483647; | ||
| 371 | + files = ( | ||
| 372 | + ); | ||
| 373 | + inputFileListPaths = ( | ||
| 374 | + "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist", | ||
| 375 | + ); | ||
| 376 | + name = "[CP] Embed Pods Frameworks"; | ||
| 377 | + outputFileListPaths = ( | ||
| 378 | + "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist", | ||
| 379 | + ); | ||
| 380 | + runOnlyForDeploymentPostprocessing = 0; | ||
| 381 | + shellPath = /bin/sh; | ||
| 382 | + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks.sh\"\n"; | ||
| 383 | + showEnvVarsInLog = 0; | ||
| 384 | + }; | ||
| 367 | 4348DC7B38CB739C4ABA7DAC /* [CP] Embed Pods Frameworks */ = { | 385 | 4348DC7B38CB739C4ABA7DAC /* [CP] Embed Pods Frameworks */ = { |
| 368 | isa = PBXShellScriptBuildPhase; | 386 | isa = PBXShellScriptBuildPhase; |
| 369 | buildActionMask = 2147483647; | 387 | buildActionMask = 2147483647; |
| 1 | +import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | ||
| 1 | import 'package:get/get.dart'; | 2 | import 'package:get/get.dart'; |
| 2 | 3 | ||
| 3 | import '../apple_health_upload/apple_health_upload_api.dart'; | 4 | import '../apple_health_upload/apple_health_upload_api.dart'; |
| @@ -53,6 +54,7 @@ void registerUserSessionDeps() { | @@ -53,6 +54,7 @@ void registerUserSessionDeps() { | ||
| 53 | 54 | ||
| 54 | Get.put(UserApi(dioClient), permanent: true); | 55 | Get.put(UserApi(dioClient), permanent: true); |
| 55 | Get.put(VipApi(dioClient), permanent: true); | 56 | Get.put(VipApi(dioClient), permanent: true); |
| 57 | + Get.put(FriendApi(dioClient), permanent: true); | ||
| 56 | Get.put<ImService>(ImServiceStub(), permanent: true); | 58 | Get.put<ImService>(ImServiceStub(), permanent: true); |
| 57 | Get.put( | 59 | Get.put( |
| 58 | ThinkingDataService(Get.find<AppEnvironmentConfig>()), | 60 | ThinkingDataService(Get.find<AppEnvironmentConfig>()), |
| 1 | import 'package:doublefeel_flutter/core/theme/app_colors.dart'; | 1 | import 'package:doublefeel_flutter/core/theme/app_colors.dart'; |
| 2 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 2 | import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | 3 | import 'package:doublefeel_flutter/core/util/size_extensions.dart'; |
| 3 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | 4 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; |
| 4 | import 'package:doublefeel_flutter/r.dart'; | 5 | import 'package:doublefeel_flutter/r.dart'; |
| @@ -11,21 +12,18 @@ import '../controllers/account_settings_controller.dart'; | @@ -11,21 +12,18 @@ import '../controllers/account_settings_controller.dart'; | ||
| 11 | class AccountSettingsView extends GetView<AccountSettingsController> { | 12 | class AccountSettingsView extends GetView<AccountSettingsController> { |
| 12 | const AccountSettingsView({super.key}); | 13 | const AccountSettingsView({super.key}); |
| 13 | 14 | ||
| 14 | - static const _background = Color(0xFFF5F2FF); | ||
| 15 | - static const _danger = Color(0xFFFC4447); | ||
| 16 | - | ||
| 17 | @override | 15 | @override |
| 18 | Widget build(BuildContext context) { | 16 | Widget build(BuildContext context) { |
| 19 | return AnnotatedRegion<SystemUiOverlayStyle>( | 17 | return AnnotatedRegion<SystemUiOverlayStyle>( |
| 20 | - value: const SystemUiOverlayStyle( | 18 | + value: SystemUiOverlayStyle( |
| 21 | statusBarColor: Colors.transparent, | 19 | statusBarColor: Colors.transparent, |
| 22 | statusBarIconBrightness: Brightness.dark, | 20 | statusBarIconBrightness: Brightness.dark, |
| 23 | statusBarBrightness: Brightness.light, | 21 | statusBarBrightness: Brightness.light, |
| 24 | - systemNavigationBarColor: _background, | 22 | + systemNavigationBarColor: context.colors.backgroundPage, |
| 25 | systemNavigationBarIconBrightness: Brightness.dark, | 23 | systemNavigationBarIconBrightness: Brightness.dark, |
| 26 | ), | 24 | ), |
| 27 | child: Scaffold( | 25 | child: Scaffold( |
| 28 | - backgroundColor: _background, | 26 | + backgroundColor: context.colors.backgroundPage, |
| 29 | appBar: AppBar( | 27 | appBar: AppBar( |
| 30 | title: Text( | 28 | title: Text( |
| 31 | '账号设置', | 29 | '账号设置', |
| @@ -60,7 +58,7 @@ class AccountSettingsView extends GetView<AccountSettingsController> { | @@ -60,7 +58,7 @@ class AccountSettingsView extends GetView<AccountSettingsController> { | ||
| 60 | child: Text( | 58 | child: Text( |
| 61 | '注销账号', | 59 | '注销账号', |
| 62 | style: TextStyle( | 60 | style: TextStyle( |
| 63 | - color: _danger, | 61 | + color: context.colors.warning, |
| 64 | fontSize: 12.dp, | 62 | fontSize: 12.dp, |
| 65 | fontWeight: FontWeight.w400, | 63 | fontWeight: FontWeight.w400, |
| 66 | height: 1.25, | 64 | height: 1.25, |
| @@ -12,13 +12,10 @@ import '../controllers/submit_feedback_controller.dart'; | @@ -12,13 +12,10 @@ import '../controllers/submit_feedback_controller.dart'; | ||
| 12 | class SubmitFeedbackView extends GetView<SubmitFeedbackController> { | 12 | class SubmitFeedbackView extends GetView<SubmitFeedbackController> { |
| 13 | const SubmitFeedbackView({super.key}); | 13 | const SubmitFeedbackView({super.key}); |
| 14 | 14 | ||
| 15 | - static const _background = Color(0xFFF5F2FF); | ||
| 16 | - | ||
| 17 | @override | 15 | @override |
| 18 | Widget build(BuildContext context) { | 16 | Widget build(BuildContext context) { |
| 19 | return Scaffold( | 17 | return Scaffold( |
| 20 | resizeToAvoidBottomInset: true, | 18 | resizeToAvoidBottomInset: true, |
| 21 | - backgroundColor: _background, | ||
| 22 | appBar: AppBar( | 19 | appBar: AppBar( |
| 23 | backgroundColor: Colors.transparent, | 20 | backgroundColor: Colors.transparent, |
| 24 | surfaceTintColor: Colors.transparent, | 21 | surfaceTintColor: Colors.transparent, |
| 1 | import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; | 1 | import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; |
| 2 | +import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | ||
| 2 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 3 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 3 | -import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | ||
| 4 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; | 4 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; |
| 5 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 5 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 6 | import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart'; | 6 | import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart'; |
| 7 | +import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; | ||
| 7 | import 'package:get/get.dart'; | 8 | import 'package:get/get.dart'; |
| 8 | 9 | ||
| 9 | -/// 跳转到好友主页时需要传入的参数。 | ||
| 10 | -class FriendHomeArguments { | ||
| 11 | - const FriendHomeArguments({ | ||
| 12 | - required this.userId, | ||
| 13 | - this.name, | ||
| 14 | - this.avatarUrl, | ||
| 15 | - }); | ||
| 16 | - | ||
| 17 | - final int userId; | ||
| 18 | - final String? name; | ||
| 19 | - final String? avatarUrl; | ||
| 20 | -} | ||
| 21 | - | ||
| 22 | class FriendHomeBinding extends Bindings { | 10 | class FriendHomeBinding extends Bindings { |
| 23 | @override | 11 | @override |
| 24 | void dependencies() { | 12 | void dependencies() { |
| 25 | - final args = Get.arguments as FriendHomeArguments; | 13 | + final args = Get.arguments as FriendItem; |
| 26 | Get.lazyPut<TodayController>( | 14 | Get.lazyPut<TodayController>( |
| 27 | () => TodayController( | 15 | () => TodayController( |
| 28 | - Get.find<UserApi>(), | ||
| 29 | Get.find<HealthApi>(), | 16 | Get.find<HealthApi>(), |
| 17 | + Get.find<FriendApi>(), | ||
| 30 | Get.find<UserStateService>(), | 18 | Get.find<UserStateService>(), |
| 31 | Get.find<HealthKitUploadService>(), | 19 | Get.find<HealthKitUploadService>(), |
| 32 | Get.find<AppleHealthUploadTool>(), | 20 | Get.find<AppleHealthUploadTool>(), |
| 33 | - friendUserId: args.userId, // 好友 userId,驱动 isFriend = true | 21 | + friendInfo: args, |
| 34 | ), | 22 | ), |
| 35 | tag: 'friend_${args.userId}', // 动态 tag,支持多个好友同时在路由栈中 | 23 | tag: 'friend_${args.userId}', // 动态 tag,支持多个好友同时在路由栈中 |
| 36 | ); | 24 | ); |
| 1 | import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; | 1 | import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; |
| 2 | import 'package:doublefeel_flutter/app/modules/home/views/tabs/today_tab.dart'; | 2 | import 'package:doublefeel_flutter/app/modules/home/views/tabs/today_tab.dart'; |
| 3 | +import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; | ||
| 3 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| 4 | import 'package:get/get.dart'; | 5 | import 'package:get/get.dart'; |
| 5 | 6 | ||
| 6 | -import '../bindings/friend_home_binding.dart'; | ||
| 7 | - | ||
| 8 | class FriendHomePage extends GetView<TodayController> { | 7 | class FriendHomePage extends GetView<TodayController> { |
| 9 | const FriendHomePage({super.key}); | 8 | const FriendHomePage({super.key}); |
| 10 | 9 | ||
| 11 | - FriendHomeArguments get _args => Get.arguments as FriendHomeArguments; | 10 | + FriendItem get _args => Get.arguments as FriendItem; |
| 12 | 11 | ||
| 13 | /// 与 FriendHomeBinding 中注册的 tag 保持一致 | 12 | /// 与 FriendHomeBinding 中注册的 tag 保持一致 |
| 14 | @override | 13 | @override |
| 1 | import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart'; | 1 | import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart'; |
| 2 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | 2 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; |
| 3 | +import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | ||
| 3 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 4 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 4 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 5 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 5 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; | 6 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; |
| @@ -17,8 +18,8 @@ class HomeBinding extends Bindings { | @@ -17,8 +18,8 @@ class HomeBinding extends Bindings { | ||
| 17 | Get.lazyPut<HomeController>(() => HomeController(), fenix: true); | 18 | Get.lazyPut<HomeController>(() => HomeController(), fenix: true); |
| 18 | Get.lazyPut<TodayController>( | 19 | Get.lazyPut<TodayController>( |
| 19 | () => TodayController( | 20 | () => TodayController( |
| 20 | - Get.find<UserApi>(), | ||
| 21 | Get.find<HealthApi>(), | 21 | Get.find<HealthApi>(), |
| 22 | + Get.find<FriendApi>(), | ||
| 22 | Get.find<UserStateService>(), | 23 | Get.find<UserStateService>(), |
| 23 | Get.find<HealthKitUploadService>(), | 24 | Get.find<HealthKitUploadService>(), |
| 24 | Get.find<AppleHealthUploadTool>(), | 25 | Get.find<AppleHealthUploadTool>(), |
| @@ -5,15 +5,18 @@ import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_t | @@ -5,15 +5,18 @@ import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_t | ||
| 5 | import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart'; | 5 | import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart'; |
| 6 | import 'package:doublefeel_flutter/app/modules/home/controllers/home_controller.dart'; | 6 | import 'package:doublefeel_flutter/app/modules/home/controllers/home_controller.dart'; |
| 7 | import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_controller.dart'; | 7 | import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_controller.dart'; |
| 8 | +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'; | 9 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; |
| 9 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 10 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 10 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | 11 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; |
| 12 | +import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | ||
| 11 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 13 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 12 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 14 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 13 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 15 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 14 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; | 16 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; |
| 15 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 17 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 16 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; | 18 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; |
| 19 | +import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; | ||
| 17 | import 'package:doublefeel_flutter/data/models/health/health_models.dart'; | 20 | import 'package:doublefeel_flutter/data/models/health/health_models.dart'; |
| 18 | import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; | 21 | import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; |
| 19 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; | 22 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; |
| @@ -46,26 +49,28 @@ class HrvAnnotation { | @@ -46,26 +49,28 @@ class HrvAnnotation { | ||
| 46 | 49 | ||
| 47 | class TodayController extends GetxController { | 50 | class TodayController extends GetxController { |
| 48 | TodayController( | 51 | TodayController( |
| 49 | - this._userApi, | ||
| 50 | this._healthApi, | 52 | this._healthApi, |
| 53 | + this._friendApi, | ||
| 51 | this._userStateService, | 54 | this._userStateService, |
| 52 | this._healthKitUploadService, | 55 | this._healthKitUploadService, |
| 53 | this._appleHealthUploadTool, { | 56 | this._appleHealthUploadTool, { |
| 54 | - this.friendUserId, // null = 自己,非 null = 好友 | ||
| 55 | - }); | 57 | + FriendItem? friendInfo, // null = 自己,非 null = 好友 |
| 58 | + }) : _initialFriendInfo = friendInfo; | ||
| 56 | 59 | ||
| 57 | - final UserApi _userApi; | ||
| 58 | final HealthApi _healthApi; | 60 | final HealthApi _healthApi; |
| 61 | + final FriendApi _friendApi; | ||
| 59 | final UserStateService _userStateService; | 62 | final UserStateService _userStateService; |
| 60 | final HealthKitUploadService _healthKitUploadService; | 63 | final HealthKitUploadService _healthKitUploadService; |
| 61 | final AppleHealthUploadTool _appleHealthUploadTool; | 64 | final AppleHealthUploadTool _appleHealthUploadTool; |
| 62 | final HealthKitHostApi _hostApi = HealthKitHostApi(); | 65 | final HealthKitHostApi _hostApi = HealthKitHostApi(); |
| 63 | 66 | ||
| 64 | - /// 好友的 userId;null 表示查看自己的数据,非 null 表示查看好友的数据。 | ||
| 65 | - final int? friendUserId; | 67 | + /// 好友的信息;null 表示查看自己的数据,非 null 表示查看好友的数据。 |
| 68 | + final FriendItem? _initialFriendInfo; | ||
| 69 | + final targetFriendInfo = Rxn<FriendItem>(); | ||
| 66 | 70 | ||
| 67 | /// 是否正在查看好友数据。 | 71 | /// 是否正在查看好友数据。 |
| 68 | - bool get isFriend => friendUserId != null; | 72 | + bool get isFriend => targetFriendInfo.value != null; |
| 73 | + int? get friendUserId => targetFriendInfo.value?.friendUserId; | ||
| 69 | 74 | ||
| 70 | UserStateService get userStateService => _userStateService; | 75 | UserStateService get userStateService => _userStateService; |
| 71 | 76 | ||
| @@ -140,6 +145,7 @@ class TodayController extends GetxController { | @@ -140,6 +145,7 @@ class TodayController extends GetxController { | ||
| 140 | @override | 145 | @override |
| 141 | void onInit() { | 146 | void onInit() { |
| 142 | super.onInit(); | 147 | super.onInit(); |
| 148 | + targetFriendInfo.value = _initialFriendInfo; | ||
| 143 | final today = DateUtils.dateOnly(DateTime.now()); | 149 | final today = DateUtils.dateOnly(DateTime.now()); |
| 144 | firstSelectableDay = DateTime(today.year - 1); | 150 | firstSelectableDay = DateTime(today.year - 1); |
| 145 | lastSelectableDay = today; | 151 | lastSelectableDay = today; |
| @@ -497,11 +503,7 @@ class TodayController extends GetxController { | @@ -497,11 +503,7 @@ class TodayController extends GetxController { | ||
| 497 | if (isFriend) { | 503 | if (isFriend) { |
| 498 | Get.toNamed( | 504 | Get.toNamed( |
| 499 | Routes.FRIEND_TREND, | 505 | Routes.FRIEND_TREND, |
| 500 | - arguments: FriendTrendArguments( | ||
| 501 | - userId: 58, | ||
| 502 | - name: 'xxa', | ||
| 503 | - avatarUrl: 'asdfa', | ||
| 504 | - ), | 506 | + arguments: getFriendTrendArguments(), |
| 505 | ); | 507 | ); |
| 506 | } else { | 508 | } else { |
| 507 | Get.find<HomeController>().openTrend(TrendType.hrv); | 509 | Get.find<HomeController>().openTrend(TrendType.hrv); |
| @@ -510,14 +512,7 @@ class TodayController extends GetxController { | @@ -510,14 +512,7 @@ class TodayController extends GetxController { | ||
| 510 | 512 | ||
| 511 | toTrendSleepPage() { | 513 | toTrendSleepPage() { |
| 512 | if (isFriend) { | 514 | if (isFriend) { |
| 513 | - Get.toNamed( | ||
| 514 | - Routes.FRIEND_TREND, | ||
| 515 | - arguments: FriendTrendArguments( | ||
| 516 | - userId: 58, | ||
| 517 | - name: 'xxa', | ||
| 518 | - avatarUrl: 'asdfa', | ||
| 519 | - ), | ||
| 520 | - ); | 515 | + Get.toNamed(Routes.FRIEND_TREND, arguments: getFriendTrendArguments()); |
| 521 | } else { | 516 | } else { |
| 522 | Get.find<HomeController>().openTrend(TrendType.sleep); | 517 | Get.find<HomeController>().openTrend(TrendType.sleep); |
| 523 | } | 518 | } |
| @@ -525,16 +520,61 @@ class TodayController extends GetxController { | @@ -525,16 +520,61 @@ class TodayController extends GetxController { | ||
| 525 | 520 | ||
| 526 | toTrendActivityPage() { | 521 | toTrendActivityPage() { |
| 527 | if (isFriend) { | 522 | if (isFriend) { |
| 528 | - Get.toNamed( | ||
| 529 | - Routes.FRIEND_TREND, | ||
| 530 | - arguments: FriendTrendArguments( | ||
| 531 | - userId: 58, | ||
| 532 | - name: 'xxa', | ||
| 533 | - avatarUrl: 'asdfa', | ||
| 534 | - ), | ||
| 535 | - ); | 523 | + Get.toNamed(Routes.FRIEND_TREND, arguments: getFriendTrendArguments()); |
| 536 | } else { | 524 | } else { |
| 537 | Get.find<HomeController>().openTrend(TrendType.activity); | 525 | Get.find<HomeController>().openTrend(TrendType.activity); |
| 538 | } | 526 | } |
| 539 | } | 527 | } |
| 528 | + | ||
| 529 | + getFriendTrendArguments() { | ||
| 530 | + var value = targetFriendInfo.value; | ||
| 531 | + if (value != null && value.friendUserId != null) { | ||
| 532 | + return FriendTrendArguments( | ||
| 533 | + userId: value.friendUserId!, | ||
| 534 | + name: value.remarkName ?? '', | ||
| 535 | + avatarUrl: value.avatar, | ||
| 536 | + ); | ||
| 537 | + } else { | ||
| 538 | + return null; | ||
| 539 | + } | ||
| 540 | + } | ||
| 541 | + | ||
| 542 | + void selectFriend(FriendItem friendInfo) { | ||
| 543 | + if (friendInfo.friendUserId != targetFriendInfo.value?.friendUserId) { | ||
| 544 | + targetFriendInfo.value = friendInfo; | ||
| 545 | + _clearRealTimeData(); | ||
| 546 | + loadDataForDate(selectedDate.value); | ||
| 547 | + } | ||
| 548 | + } | ||
| 549 | + | ||
| 550 | + showFriendListBottomSheet() async { | ||
| 551 | + _friendApi.friendList(false).then( | ||
| 552 | + (res) { | ||
| 553 | + switch (res) { | ||
| 554 | + case AppSuccess(:final data): | ||
| 555 | + friendsList.assignAll(data.list); | ||
| 556 | + case AppFailure(): | ||
| 557 | + } | ||
| 558 | + }, | ||
| 559 | + ); | ||
| 560 | + | ||
| 561 | + Get.bottomSheet( | ||
| 562 | + DraggableScrollableSheet( | ||
| 563 | + maxChildSize: 0.6, | ||
| 564 | + initialChildSize: 0.6, | ||
| 565 | + expand: false, | ||
| 566 | + snap: true, | ||
| 567 | + builder: (context, scrollController) { | ||
| 568 | + return FriendSelectBottomSheet( | ||
| 569 | + scrollController: scrollController, controller: this); | ||
| 570 | + }, | ||
| 571 | + ), | ||
| 572 | + barrierColor: Colors.black.withValues(alpha: 0.7), | ||
| 573 | + enableDrag: true, | ||
| 574 | + isScrollControlled: true, | ||
| 575 | + persistent: false, | ||
| 576 | + ); | ||
| 577 | + } | ||
| 578 | + | ||
| 579 | + RxList<FriendItem> friendsList = RxList.empty(); | ||
| 540 | } | 580 | } |
| @@ -21,7 +21,6 @@ class HomePage extends GetView<HomeController> { | @@ -21,7 +21,6 @@ class HomePage extends GetView<HomeController> { | ||
| 21 | @override | 21 | @override |
| 22 | Widget build(BuildContext context) { | 22 | Widget build(BuildContext context) { |
| 23 | return Scaffold( | 23 | return Scaffold( |
| 24 | - backgroundColor: const Color(0xFFF5F2FF), | ||
| 25 | // 不使用系统 AppBar,Today 页自带状态栏适配 | 24 | // 不使用系统 AppBar,Today 页自带状态栏适配 |
| 26 | extendBody: true, // 让内容延伸到 bottomNavigationBar 下方 | 25 | extendBody: true, // 让内容延伸到 bottomNavigationBar 下方 |
| 27 | extendBodyBehindAppBar: true, | 26 | extendBodyBehindAppBar: true, |
| 1 | import 'package:cached_network_image/cached_network_image.dart'; | 1 | import 'package:cached_network_image/cached_network_image.dart'; |
| 2 | import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; | 2 | import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; |
| 3 | import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart'; | 3 | import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart'; |
| 4 | -import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart'; | ||
| 5 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | 4 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; |
| 6 | import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_view.dart'; | 5 | import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_view.dart'; |
| 7 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; | 6 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; |
| @@ -10,6 +9,7 @@ import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; | @@ -10,6 +9,7 @@ import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; | ||
| 10 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 9 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| 11 | import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | 10 | import 'package:doublefeel_flutter/core/util/size_extensions.dart'; |
| 12 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 11 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 12 | +import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; | ||
| 13 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | 13 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; |
| 14 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | 14 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; |
| 15 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 15 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| @@ -22,14 +22,12 @@ import '../../../../../core/theme/app_colors.dart'; | @@ -22,14 +22,12 @@ import '../../../../../core/theme/app_colors.dart'; | ||
| 22 | class MyTab extends GetView<MyController> { | 22 | class MyTab extends GetView<MyController> { |
| 23 | const MyTab({super.key}); | 23 | const MyTab({super.key}); |
| 24 | 24 | ||
| 25 | - static const _bgColor = Color(0xFFF5F2FF); | ||
| 26 | - | ||
| 27 | @override | 25 | @override |
| 28 | Widget build(BuildContext context) { | 26 | Widget build(BuildContext context) { |
| 29 | final userPrefs = Get.find<UserPreferencesStorage>(); | 27 | final userPrefs = Get.find<UserPreferencesStorage>(); |
| 30 | 28 | ||
| 31 | return Container( | 29 | return Container( |
| 32 | - color: _bgColor, | 30 | + color: context.colors.backgroundPage, |
| 33 | child: SafeArea( | 31 | child: SafeArea( |
| 34 | bottom: false, | 32 | bottom: false, |
| 35 | child: Obx(() { | 33 | child: Obx(() { |
| @@ -80,7 +78,12 @@ class MyTab extends GetView<MyController> { | @@ -80,7 +78,12 @@ class MyTab extends GetView<MyController> { | ||
| 80 | _SettingsRow( | 78 | _SettingsRow( |
| 81 | title: 'Route List', | 79 | title: 'Route List', |
| 82 | onTap: () { | 80 | onTap: () { |
| 83 | - Get.toNamed(Routes.ROUTE_LIST); | 81 | + Get.toNamed(Routes.FRIEND_HOME, |
| 82 | + arguments: FriendItem( | ||
| 83 | + friendUserId: 61, | ||
| 84 | + remarkName: '徐璐', | ||
| 85 | + avatar: | ||
| 86 | + 'https://cdn3.didiapp.com/manager_upload/ab2abeecb14442b198bdaedc7c41f094.png')); | ||
| 84 | }, | 87 | }, |
| 85 | ), | 88 | ), |
| 86 | ], | 89 | ], |
| 1 | import 'package:cached_network_image/cached_network_image.dart'; | 1 | import 'package:cached_network_image/cached_network_image.dart'; |
| 2 | + | ||
| 2 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measurement_bottom_sheet.dart'; | 3 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measurement_bottom_sheet.dart'; |
| 3 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; | 4 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; |
| 4 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; | 5 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; |
| 6 | + | ||
| 5 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 7 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 6 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 8 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| 7 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 9 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| @@ -16,6 +18,7 @@ import '../../widgets/today/today_hrv_ad_banner.dart'; | @@ -16,6 +18,7 @@ import '../../widgets/today/today_hrv_ad_banner.dart'; | ||
| 16 | import '../../widgets/today/today_hrv_chart_card.dart'; | 18 | import '../../widgets/today/today_hrv_chart_card.dart'; |
| 17 | import '../../widgets/today/today_hrv_number_card.dart'; | 19 | import '../../widgets/today/today_hrv_number_card.dart'; |
| 18 | import '../../widgets/today/today_sleep_activity_cards.dart'; | 20 | import '../../widgets/today/today_sleep_activity_cards.dart'; |
| 21 | + | ||
| 19 | import '../../widgets/today/stress_status_explanation_bottom_sheet.dart'; | 22 | import '../../widgets/today/stress_status_explanation_bottom_sheet.dart'; |
| 20 | 23 | ||
| 21 | import '../../widgets/today/premium_card.dart'; | 24 | import '../../widgets/today/premium_card.dart'; |
| @@ -83,7 +86,7 @@ class TodayTabBody extends StatelessWidget { | @@ -83,7 +86,7 @@ class TodayTabBody extends StatelessWidget { | ||
| 83 | mainAxisSize: MainAxisSize.min, | 86 | mainAxisSize: MainAxisSize.min, |
| 84 | children: [ | 87 | children: [ |
| 85 | controller.isFriend | 88 | controller.isFriend |
| 86 | - ? _TopFriendBar() | 89 | + ? _TopFriendBar(controller) |
| 87 | : _TopDateBar( | 90 | : _TopDateBar( |
| 88 | title: _dateTitle( | 91 | title: _dateTitle( |
| 89 | context, controller.selectedDate.value), | 92 | context, controller.selectedDate.value), |
| @@ -562,6 +565,10 @@ class _LatestHrvCard extends StatelessWidget { | @@ -562,6 +565,10 @@ class _LatestHrvCard extends StatelessWidget { | ||
| 562 | class _TopFriendBar extends StatelessWidget { | 565 | class _TopFriendBar extends StatelessWidget { |
| 563 | final _topBarHeight = 48.0; | 566 | final _topBarHeight = 48.0; |
| 564 | 567 | ||
| 568 | + const _TopFriendBar(this.controller); | ||
| 569 | + | ||
| 570 | + final TodayController controller; | ||
| 571 | + | ||
| 565 | @override | 572 | @override |
| 566 | Widget build(BuildContext context) { | 573 | Widget build(BuildContext context) { |
| 567 | return SizedBox( | 574 | return SizedBox( |
| @@ -572,7 +579,8 @@ class _TopFriendBar extends StatelessWidget { | @@ -572,7 +579,8 @@ class _TopFriendBar extends StatelessWidget { | ||
| 572 | children: [ | 579 | children: [ |
| 573 | IconButton( | 580 | IconButton( |
| 574 | highlightColor: Colors.transparent, | 581 | highlightColor: Colors.transparent, |
| 575 | - padding: EdgeInsets.zero, | 582 | + padding: EdgeInsets.only(right: 2), |
| 583 | + alignment: Alignment.centerRight, | ||
| 576 | onPressed: Get.back, | 584 | onPressed: Get.back, |
| 577 | icon: Image.asset( | 585 | icon: Image.asset( |
| 578 | 'assets/images/common/ic_nav_back.webp', | 586 | 'assets/images/common/ic_nav_back.webp', |
| @@ -583,17 +591,21 @@ class _TopFriendBar extends StatelessWidget { | @@ -583,17 +591,21 @@ class _TopFriendBar extends StatelessWidget { | ||
| 583 | Container( | 591 | Container( |
| 584 | width: 28, | 592 | width: 28, |
| 585 | height: 28, | 593 | height: 28, |
| 586 | - margin: EdgeInsets.only(left: 4, right: 8), | 594 | + margin: EdgeInsets.only(right: 4), |
| 587 | decoration: ShapeDecoration( | 595 | decoration: ShapeDecoration( |
| 596 | + image: DecorationImage( | ||
| 597 | + image: CachedNetworkImageProvider( | ||
| 598 | + '${controller.targetFriendInfo.value?.avatar}'), | ||
| 599 | + fit: BoxFit.cover, | ||
| 600 | + ), | ||
| 588 | shape: RoundedRectangleBorder( | 601 | shape: RoundedRectangleBorder( |
| 589 | side: BorderSide(width: 0.78, color: Colors.white), | 602 | side: BorderSide(width: 0.78, color: Colors.white), |
| 590 | borderRadius: BorderRadius.circular(35.78), | 603 | borderRadius: BorderRadius.circular(35.78), |
| 591 | ), | 604 | ), |
| 592 | ), | 605 | ), |
| 593 | - child: CachedNetworkImage(imageUrl: 'imageUrl'), | ||
| 594 | ), | 606 | ), |
| 595 | Text( | 607 | Text( |
| 596 | - '男朋友的状态', | 608 | + '${controller.targetFriendInfo.value?.remarkName}的状态', |
| 597 | style: TextStyle( | 609 | style: TextStyle( |
| 598 | color: Colors.black, | 610 | color: Colors.black, |
| 599 | fontSize: 16, | 611 | fontSize: 16, |
| @@ -604,7 +616,9 @@ class _TopFriendBar extends StatelessWidget { | @@ -604,7 +616,9 @@ class _TopFriendBar extends StatelessWidget { | ||
| 604 | IconButton( | 616 | IconButton( |
| 605 | highlightColor: Colors.transparent, | 617 | highlightColor: Colors.transparent, |
| 606 | padding: EdgeInsets.zero, | 618 | padding: EdgeInsets.zero, |
| 607 | - onPressed: Get.back, | 619 | + onPressed: () { |
| 620 | + controller.showFriendListBottomSheet(); | ||
| 621 | + }, | ||
| 608 | icon: Image.asset( | 622 | icon: Image.asset( |
| 609 | 'assets/images/common/ic_replace.png', | 623 | 'assets/images/common/ic_replace.png', |
| 610 | width: 20, | 624 | width: 20, |
| @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/core/result/app_result.dart'; | @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/core/result/app_result.dart'; | ||
| 6 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 6 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 7 | import 'package:doublefeel_flutter/core/theme/app_colors.dart'; | 7 | import 'package:doublefeel_flutter/core/theme/app_colors.dart'; |
| 8 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 8 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| 9 | +import 'package:doublefeel_flutter/core/util/app_toast.dart'; | ||
| 9 | 10 | ||
| 10 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 11 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 11 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 12 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| @@ -156,6 +157,7 @@ class AccountSettingView extends GetView<MyController> { | @@ -156,6 +157,7 @@ class AccountSettingView extends GetView<MyController> { | ||
| 156 | 157 | ||
| 157 | Future<void> _logout() async { | 158 | Future<void> _logout() async { |
| 158 | await Get.find<UserStateService>().onLogout(); | 159 | await Get.find<UserStateService>().onLogout(); |
| 160 | + AppToast.show('已退出登录'); | ||
| 159 | Get.offAllNamed(AppRoutes.login); | 161 | Get.offAllNamed(AppRoutes.login); |
| 160 | } | 162 | } |
| 161 | 163 | ||
| @@ -164,6 +166,7 @@ class AccountSettingView extends GetView<MyController> { | @@ -164,6 +166,7 @@ class AccountSettingView extends GetView<MyController> { | ||
| 164 | if (deleteResult is! AppSuccess<void>) return; | 166 | if (deleteResult is! AppSuccess<void>) return; |
| 165 | 167 | ||
| 166 | await Get.find<UserStateService>().onLogout(callServerLogout: false); | 168 | await Get.find<UserStateService>().onLogout(callServerLogout: false); |
| 169 | + AppToast.show('账号注销成功'); | ||
| 167 | Get.offAllNamed(AppRoutes.login); | 170 | Get.offAllNamed(AppRoutes.login); |
| 168 | } | 171 | } |
| 169 | } | 172 | } |
| 1 | +import 'package:cached_network_image/cached_network_image.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; | ||
| 3 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; | ||
| 5 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 6 | +import 'package:flutter/material.dart'; | ||
| 7 | +import 'package:get/get.dart'; | ||
| 8 | + | ||
| 9 | +class FriendSelectBottomSheet extends StatelessWidget { | ||
| 10 | + const FriendSelectBottomSheet({ | ||
| 11 | + super.key, | ||
| 12 | + required this.scrollController, | ||
| 13 | + required this.controller, | ||
| 14 | + }); | ||
| 15 | + | ||
| 16 | + final ScrollController scrollController; | ||
| 17 | + final TodayController controller; | ||
| 18 | + | ||
| 19 | + @override | ||
| 20 | + Widget build(BuildContext context) { | ||
| 21 | + final l10n = context.l10n; | ||
| 22 | + | ||
| 23 | + return Container( | ||
| 24 | + decoration: BoxDecoration( | ||
| 25 | + color: context.colors.backgroundPage, | ||
| 26 | + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), | ||
| 27 | + ), | ||
| 28 | + child: SafeArea( | ||
| 29 | + top: false, | ||
| 30 | + child: Column( | ||
| 31 | + children: [ | ||
| 32 | + SizedBox( | ||
| 33 | + height: 64, | ||
| 34 | + child: Stack( | ||
| 35 | + alignment: Alignment.center, | ||
| 36 | + children: [ | ||
| 37 | + Center( | ||
| 38 | + child: Text( | ||
| 39 | + '选择好友', | ||
| 40 | + style: TextStyle( | ||
| 41 | + color: context.colors.textPrimary, | ||
| 42 | + fontSize: 16, | ||
| 43 | + fontWeight: FontWeight.w600, | ||
| 44 | + ), | ||
| 45 | + ), | ||
| 46 | + ), | ||
| 47 | + Positioned( | ||
| 48 | + left: 16, | ||
| 49 | + child: GestureDetector( | ||
| 50 | + behavior: HitTestBehavior.opaque, | ||
| 51 | + onTap: Get.back, | ||
| 52 | + child: SizedBox( | ||
| 53 | + width: 44, | ||
| 54 | + height: 44, | ||
| 55 | + child: Center( | ||
| 56 | + child: Image.asset( | ||
| 57 | + 'assets/images/common/ic_close.png', | ||
| 58 | + width: 20, | ||
| 59 | + height: 20, | ||
| 60 | + color: context.colors.chartPurple, | ||
| 61 | + ), | ||
| 62 | + ), | ||
| 63 | + ), | ||
| 64 | + ), | ||
| 65 | + ), | ||
| 66 | + ], | ||
| 67 | + ), | ||
| 68 | + ), | ||
| 69 | + Expanded( | ||
| 70 | + child: Obx(() { | ||
| 71 | + var friendsList = controller.friendsList; | ||
| 72 | + var currentSelectedId = controller.friendUserId; | ||
| 73 | + return ListView.separated( | ||
| 74 | + controller: scrollController, | ||
| 75 | + shrinkWrap: true, | ||
| 76 | + physics: const NeverScrollableScrollPhysics(), | ||
| 77 | + padding: const EdgeInsets.fromLTRB(16, 6, 16, 16), | ||
| 78 | + itemCount: friendsList.length, | ||
| 79 | + separatorBuilder: (_, __) => const SizedBox(height: 8), | ||
| 80 | + itemBuilder: (context, index) { | ||
| 81 | + final friend = friendsList[index]; | ||
| 82 | + final isSelected = friend.friendUserId == currentSelectedId; | ||
| 83 | + return _BottomSheetFriendRow( | ||
| 84 | + friend: friend, | ||
| 85 | + isSelected: isSelected, | ||
| 86 | + onTap: () { | ||
| 87 | + controller.selectFriend(friend); | ||
| 88 | + }, | ||
| 89 | + ); | ||
| 90 | + }, | ||
| 91 | + ); | ||
| 92 | + }), | ||
| 93 | + ), | ||
| 94 | + ], | ||
| 95 | + ), | ||
| 96 | + ), | ||
| 97 | + ); | ||
| 98 | + } | ||
| 99 | +} | ||
| 100 | + | ||
| 101 | +class _BottomSheetFriendRow extends StatelessWidget { | ||
| 102 | + const _BottomSheetFriendRow({ | ||
| 103 | + required this.friend, | ||
| 104 | + required this.isSelected, | ||
| 105 | + required this.onTap, | ||
| 106 | + }); | ||
| 107 | + | ||
| 108 | + final FriendItem friend; | ||
| 109 | + final bool isSelected; | ||
| 110 | + final VoidCallback onTap; | ||
| 111 | + | ||
| 112 | + @override | ||
| 113 | + Widget build(BuildContext context) { | ||
| 114 | + return GestureDetector( | ||
| 115 | + onTap: onTap, | ||
| 116 | + child: Container( | ||
| 117 | + height: 56, | ||
| 118 | + decoration: BoxDecoration( | ||
| 119 | + color: Colors.white, | ||
| 120 | + borderRadius: BorderRadius.circular(16), | ||
| 121 | + border: isSelected | ||
| 122 | + ? Border.all(color: const Color(0xFF845EEE), width: 1) | ||
| 123 | + : null, | ||
| 124 | + ), | ||
| 125 | + child: Row( | ||
| 126 | + children: [ | ||
| 127 | + const SizedBox(width: 20), | ||
| 128 | + // Avatar | ||
| 129 | + Container( | ||
| 130 | + width: 28, | ||
| 131 | + height: 28, | ||
| 132 | + decoration: BoxDecoration( | ||
| 133 | + shape: BoxShape.circle, | ||
| 134 | + border: Border.all( | ||
| 135 | + color: const Color(0xFF845EEE).withValues(alpha: 0.3), | ||
| 136 | + width: 0.8, | ||
| 137 | + ), | ||
| 138 | + ), | ||
| 139 | + child: ClipOval( | ||
| 140 | + child: CachedNetworkImage( | ||
| 141 | + imageUrl: friend.avatar!, | ||
| 142 | + width: 28, | ||
| 143 | + height: 28, | ||
| 144 | + fit: BoxFit.cover, | ||
| 145 | + )), | ||
| 146 | + ), | ||
| 147 | + const SizedBox(width: 8), | ||
| 148 | + // Name | ||
| 149 | + Expanded( | ||
| 150 | + child: Text( | ||
| 151 | + friend.remarkName ?? '未知好友', | ||
| 152 | + style: const TextStyle( | ||
| 153 | + color: Color(0xFF0F0F11), | ||
| 154 | + fontSize: 14, | ||
| 155 | + fontWeight: FontWeight.w500, | ||
| 156 | + ), | ||
| 157 | + overflow: TextOverflow.ellipsis, | ||
| 158 | + ), | ||
| 159 | + ), | ||
| 160 | + // Checkbox | ||
| 161 | + Container( | ||
| 162 | + width: 20, | ||
| 163 | + height: 20, | ||
| 164 | + margin: const EdgeInsets.only(right: 16), | ||
| 165 | + decoration: BoxDecoration( | ||
| 166 | + shape: BoxShape.circle, | ||
| 167 | + color: | ||
| 168 | + isSelected ? const Color(0xFF845EEE) : Colors.transparent, | ||
| 169 | + border: isSelected | ||
| 170 | + ? null | ||
| 171 | + : Border.all( | ||
| 172 | + color: const Color(0xFF0F0F11).withValues(alpha: 0.2), | ||
| 173 | + width: 1.5, | ||
| 174 | + ), | ||
| 175 | + ), | ||
| 176 | + child: isSelected | ||
| 177 | + ? const Icon(Icons.check, color: Colors.white, size: 12) | ||
| 178 | + : null, | ||
| 179 | + ), | ||
| 180 | + ], | ||
| 181 | + ), | ||
| 182 | + ), | ||
| 183 | + ); | ||
| 184 | + } | ||
| 185 | +} |
| @@ -24,9 +24,9 @@ class HrvMeasurementBottomSheet extends StatelessWidget { | @@ -24,9 +24,9 @@ class HrvMeasurementBottomSheet extends StatelessWidget { | ||
| 24 | final l10n = context.l10n; | 24 | final l10n = context.l10n; |
| 25 | 25 | ||
| 26 | return Container( | 26 | return Container( |
| 27 | - decoration: const BoxDecoration( | ||
| 28 | - color: Color(0xFFF5F2FF), | ||
| 29 | - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), | 27 | + decoration: BoxDecoration( |
| 28 | + color: context.colors.backgroundPage, | ||
| 29 | + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), | ||
| 30 | ), | 30 | ), |
| 31 | child: SafeArea( | 31 | child: SafeArea( |
| 32 | top: false, | 32 | top: false, |
| @@ -41,9 +41,9 @@ class HrvPrincipleExplanationBottomSheet extends StatelessWidget { | @@ -41,9 +41,9 @@ class HrvPrincipleExplanationBottomSheet extends StatelessWidget { | ||
| 41 | final l10n = context.l10n; | 41 | final l10n = context.l10n; |
| 42 | 42 | ||
| 43 | return Container( | 43 | return Container( |
| 44 | - decoration: const BoxDecoration( | ||
| 45 | - color: Color(0xFFF5F2FF), | ||
| 46 | - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), | 44 | + decoration: BoxDecoration( |
| 45 | + color: context.colors.backgroundPage, | ||
| 46 | + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), | ||
| 47 | ), | 47 | ), |
| 48 | child: SafeArea( | 48 | child: SafeArea( |
| 49 | top: false, | 49 | top: false, |
| @@ -10,8 +10,6 @@ class NoHealthDataPage extends StatelessWidget { | @@ -10,8 +10,6 @@ class NoHealthDataPage extends StatelessWidget { | ||
| 10 | this.onHelp, | 10 | this.onHelp, |
| 11 | }); | 11 | }); |
| 12 | 12 | ||
| 13 | - static const _backgroundColor = Color(0xFFF5F2FF); | ||
| 14 | - | ||
| 15 | static const _placeholderColor = Color(0xFFD9D9D9); | 13 | static const _placeholderColor = Color(0xFFD9D9D9); |
| 16 | 14 | ||
| 17 | final VoidCallback? onRefresh; | 15 | final VoidCallback? onRefresh; |
| @@ -21,10 +19,10 @@ class NoHealthDataPage extends StatelessWidget { | @@ -21,10 +19,10 @@ class NoHealthDataPage extends StatelessWidget { | ||
| 21 | Widget build(BuildContext context) { | 19 | Widget build(BuildContext context) { |
| 22 | return AnnotatedRegion<SystemUiOverlayStyle>( | 20 | return AnnotatedRegion<SystemUiOverlayStyle>( |
| 23 | value: AppTheme.systemUiOverlayStyle.copyWith( | 21 | value: AppTheme.systemUiOverlayStyle.copyWith( |
| 24 | - systemNavigationBarColor: _backgroundColor, | 22 | + systemNavigationBarColor: context.colors.backgroundPage, |
| 25 | ), | 23 | ), |
| 26 | child: Scaffold( | 24 | child: Scaffold( |
| 27 | - backgroundColor: _backgroundColor, | 25 | + backgroundColor: context.colors.backgroundPage, |
| 28 | body: Column( | 26 | body: Column( |
| 29 | children: [ | 27 | children: [ |
| 30 | SafeArea( | 28 | SafeArea( |
| @@ -41,8 +41,8 @@ class RealtimeStressExplanationBottomSheet extends StatelessWidget { | @@ -41,8 +41,8 @@ class RealtimeStressExplanationBottomSheet extends StatelessWidget { | ||
| 41 | final l10n = context.l10n; | 41 | final l10n = context.l10n; |
| 42 | 42 | ||
| 43 | return Container( | 43 | return Container( |
| 44 | - decoration: const BoxDecoration( | ||
| 45 | - color: Color(0xFFF5F2FF), | 44 | + decoration: BoxDecoration( |
| 45 | + color: context.colors.backgroundPage, | ||
| 46 | borderRadius: BorderRadius.vertical(top: Radius.circular(16)), | 46 | borderRadius: BorderRadius.vertical(top: Radius.circular(16)), |
| 47 | ), | 47 | ), |
| 48 | child: SafeArea( | 48 | child: SafeArea( |
| @@ -41,8 +41,8 @@ class TodayFaqBottomSheet extends StatelessWidget { | @@ -41,8 +41,8 @@ class TodayFaqBottomSheet extends StatelessWidget { | ||
| 41 | final l10n = context.l10n; | 41 | final l10n = context.l10n; |
| 42 | 42 | ||
| 43 | return Container( | 43 | return Container( |
| 44 | - decoration: const BoxDecoration( | ||
| 45 | - color: Color(0xFFF5F2FF), | 44 | + decoration: BoxDecoration( |
| 45 | + color: context.colors.backgroundPage, | ||
| 46 | borderRadius: BorderRadius.vertical(top: Radius.circular(16)), | 46 | borderRadius: BorderRadius.vertical(top: Radius.circular(16)), |
| 47 | ), | 47 | ), |
| 48 | child: SafeArea( | 48 | child: SafeArea( |
| @@ -28,9 +28,11 @@ class TodaySleepCard extends StatelessWidget { | @@ -28,9 +28,11 @@ class TodaySleepCard extends StatelessWidget { | ||
| 28 | (healthData?.hrAvg == null || healthData?.hrAvg == 0) | 28 | (healthData?.hrAvg == null || healthData?.hrAvg == 0) |
| 29 | ? '--' | 29 | ? '--' |
| 30 | : '${healthData?.hrAvg}'; | 30 | : '${healthData?.hrAvg}'; |
| 31 | - double sleepDurationProgress = | ||
| 32 | - ((healthData?.sleepDurationScore() ?? 0) * 3.60); | ||
| 33 | - double sleepScoreProgress = (healthData?.sleepScore ?? 0.0) * 3.6; | 31 | + double sleepDurationProgress = sleepDuration == 0 |
| 32 | + ? -1 | ||
| 33 | + : ((healthData?.sleepDurationScore() ?? 0) * 3.60); | ||
| 34 | + double sleepScoreProgress = | ||
| 35 | + sleepDuration == 0 ? -1 : (healthData?.sleepScore ?? 0.0) * 3.6; | ||
| 34 | 36 | ||
| 35 | return _TodaySummaryCard( | 37 | return _TodaySummaryCard( |
| 36 | iconAsset: 'assets/images/common/ic_sleep_stroke.png', | 38 | iconAsset: 'assets/images/common/ic_sleep_stroke.png', |
lib/core/network/api/friend_api.dart
0 → 100644
| 1 | +import '../../../data/models/friend/friend_models.dart'; | ||
| 2 | +import '../../result/app_result.dart'; | ||
| 3 | +import '../../result/safe_call.dart'; | ||
| 4 | +import '../api_paths.dart'; | ||
| 5 | +import '../dio_client.dart'; | ||
| 6 | + | ||
| 7 | +class FriendApi { | ||
| 8 | + FriendApi(this._dioClient); | ||
| 9 | + | ||
| 10 | + final DioClient _dioClient; | ||
| 11 | + | ||
| 12 | + Future<AppResult<void>> addFriend(String uniqueCode) { | ||
| 13 | + return safeCall( | ||
| 14 | + call: () async { | ||
| 15 | + await _dioClient.dio.post( | ||
| 16 | + ApiPaths.friends, | ||
| 17 | + data: {'unique_code': uniqueCode}, | ||
| 18 | + ); | ||
| 19 | + }, | ||
| 20 | + ); | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + Future<AppResult<FriendListResponse>> friendList(bool withHealthData) { | ||
| 24 | + return safeCall( | ||
| 25 | + call: () async { | ||
| 26 | + final response = await _dioClient.dio.get( | ||
| 27 | + ApiPaths.friends, | ||
| 28 | + queryParameters: {'with_health_data': withHealthData ? 1 : 0}, | ||
| 29 | + ); | ||
| 30 | + return FriendListResponse.fromJson( | ||
| 31 | + response.data as Map<String, dynamic>, | ||
| 32 | + ); | ||
| 33 | + }, | ||
| 34 | + ); | ||
| 35 | + } | ||
| 36 | +} |
| @@ -63,4 +63,7 @@ abstract final class ApiPaths { | @@ -63,4 +63,7 @@ abstract final class ApiPaths { | ||
| 63 | static const watchThemeList = '/client/doublefeel/theme/watch_theme/list/'; | 63 | static const watchThemeList = '/client/doublefeel/theme/watch_theme/list/'; |
| 64 | static const watchThemeActive = | 64 | static const watchThemeActive = |
| 65 | '/client/doublefeel/theme/watch_theme/active/'; | 65 | '/client/doublefeel/theme/watch_theme/active/'; |
| 66 | + | ||
| 67 | + // friends | ||
| 68 | + static const friends = '/client/doublefeel/health/v2/friends/'; | ||
| 66 | } | 69 | } |
lib/data/models/friend/friend_models.dart
0 → 100644
| 1 | +// ─── Responses ─────────────────────────────────────────────────────────────── | ||
| 2 | + | ||
| 3 | +class FriendListResponse { | ||
| 4 | + const FriendListResponse({this.list = const []}); | ||
| 5 | + | ||
| 6 | + final List<FriendItem> list; | ||
| 7 | + | ||
| 8 | + factory FriendListResponse.fromJson(Map<String, dynamic> json) { | ||
| 9 | + return FriendListResponse( | ||
| 10 | + list: (json['list'] as List<dynamic>?) | ||
| 11 | + ?.map((e) => FriendItem.fromJson(e as Map<String, dynamic>)) | ||
| 12 | + .toList() ?? | ||
| 13 | + const [], | ||
| 14 | + ); | ||
| 15 | + } | ||
| 16 | + | ||
| 17 | + Map<String, dynamic> toJson() => { | ||
| 18 | + 'list': list.map((e) => e.toJson()).toList(), | ||
| 19 | + }; | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +class FriendItem { | ||
| 23 | + const FriendItem({ | ||
| 24 | + this.id, | ||
| 25 | + this.userId, | ||
| 26 | + this.friendUserId, | ||
| 27 | + this.remarkName, | ||
| 28 | + this.showInDial, | ||
| 29 | + this.state, | ||
| 30 | + this.createTime, | ||
| 31 | + this.updateTime, | ||
| 32 | + this.avatar, | ||
| 33 | + this.healthData, | ||
| 34 | + }); | ||
| 35 | + | ||
| 36 | + final int? id; | ||
| 37 | + final int? userId; | ||
| 38 | + final int? friendUserId; | ||
| 39 | + final String? remarkName; | ||
| 40 | + | ||
| 41 | + /// 0 = hidden, 1 = shown in dial | ||
| 42 | + final int? showInDial; | ||
| 43 | + | ||
| 44 | + /// 1 = normal | ||
| 45 | + final int? state; | ||
| 46 | + | ||
| 47 | + final int? createTime; | ||
| 48 | + final int? updateTime; | ||
| 49 | + final String? avatar; | ||
| 50 | + final FriendHealthData? healthData; | ||
| 51 | + | ||
| 52 | + bool get isShowInDial => showInDial == 1; | ||
| 53 | + | ||
| 54 | + factory FriendItem.fromJson(Map<String, dynamic> json) { | ||
| 55 | + return FriendItem( | ||
| 56 | + id: json['id'] as int?, | ||
| 57 | + userId: json['user_id'] as int?, | ||
| 58 | + friendUserId: json['friend_user_id'] as int?, | ||
| 59 | + remarkName: json['remark_name'] as String?, | ||
| 60 | + showInDial: json['show_in_dial'] as int?, | ||
| 61 | + state: json['state'] as int?, | ||
| 62 | + createTime: json['create_time'] as int?, | ||
| 63 | + updateTime: json['update_time'] as int?, | ||
| 64 | + avatar: json['avatar'] as String?, | ||
| 65 | + healthData: json['health_data'] == null | ||
| 66 | + ? null | ||
| 67 | + : FriendHealthData.fromJson( | ||
| 68 | + json['health_data'] as Map<String, dynamic>), | ||
| 69 | + ); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + Map<String, dynamic> toJson() { | ||
| 73 | + final val = <String, dynamic>{}; | ||
| 74 | + if (id != null) val['id'] = id; | ||
| 75 | + if (userId != null) val['user_id'] = userId; | ||
| 76 | + if (friendUserId != null) val['friend_user_id'] = friendUserId; | ||
| 77 | + if (remarkName != null) val['remark_name'] = remarkName; | ||
| 78 | + if (showInDial != null) val['show_in_dial'] = showInDial; | ||
| 79 | + if (state != null) val['state'] = state; | ||
| 80 | + if (createTime != null) val['create_time'] = createTime; | ||
| 81 | + if (updateTime != null) val['update_time'] = updateTime; | ||
| 82 | + if (avatar != null) val['avatar'] = avatar; | ||
| 83 | + if (healthData != null) val['health_data'] = healthData!.toJson(); | ||
| 84 | + return val; | ||
| 85 | + } | ||
| 86 | +} | ||
| 87 | + | ||
| 88 | +class FriendHealthData { | ||
| 89 | + const FriendHealthData({ | ||
| 90 | + this.hrvState, | ||
| 91 | + this.realtimeStress, | ||
| 92 | + this.sleepEvaluate, | ||
| 93 | + this.totalSteps, | ||
| 94 | + }); | ||
| 95 | + | ||
| 96 | + /// HRV state indicator | ||
| 97 | + final int? hrvState; | ||
| 98 | + | ||
| 99 | + /// Real-time stress data (may be an empty map `{}`) | ||
| 100 | + final Map<String, dynamic>? realtimeStress; | ||
| 101 | + | ||
| 102 | + /// Sleep quality score | ||
| 103 | + final int? sleepEvaluate; | ||
| 104 | + | ||
| 105 | + /// Total step count for the day (nullable) | ||
| 106 | + final int? totalSteps; | ||
| 107 | + | ||
| 108 | + factory FriendHealthData.fromJson(Map<String, dynamic> json) { | ||
| 109 | + return FriendHealthData( | ||
| 110 | + hrvState: json['hrv_state'] as int?, | ||
| 111 | + realtimeStress: json['realtime_stress'] as Map<String, dynamic>?, | ||
| 112 | + sleepEvaluate: json['sleep_evaluate'] as int?, | ||
| 113 | + totalSteps: json['total_steps'] as int?, | ||
| 114 | + ); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + Map<String, dynamic> toJson() { | ||
| 118 | + final val = <String, dynamic>{}; | ||
| 119 | + if (hrvState != null) val['hrv_state'] = hrvState; | ||
| 120 | + if (realtimeStress != null) val['realtime_stress'] = realtimeStress; | ||
| 121 | + if (sleepEvaluate != null) val['sleep_evaluate'] = sleepEvaluate; | ||
| 122 | + if (totalSteps != null) val['total_steps'] = totalSteps; | ||
| 123 | + return val; | ||
| 124 | + } | ||
| 125 | +} |
| @@ -98,7 +98,9 @@ class V2HealthData { | @@ -98,7 +98,9 @@ class V2HealthData { | ||
| 98 | factory V2HealthData.fromJson(Map<String, dynamic> json) { | 98 | factory V2HealthData.fromJson(Map<String, dynamic> json) { |
| 99 | return V2HealthData( | 99 | return V2HealthData( |
| 100 | hrvAvg: _parseInt(json['hrv_avg']), | 100 | hrvAvg: _parseInt(json['hrv_avg']), |
| 101 | - lastRestingHrValue: _parseInt(json['last_resting_hr_value']), | 101 | + lastRestingHrValue: json['last_resting_hr_value'] == 0 |
| 102 | + ? null | ||
| 103 | + : _parseInt(json['last_resting_hr_value']), | ||
| 102 | hrAvg: _parseInt(json['hr_avg']), | 104 | hrAvg: _parseInt(json['hr_avg']), |
| 103 | move: _parseInt(json['move']), | 105 | move: _parseInt(json['move']), |
| 104 | exercise: _parseInt(json['exercise']), | 106 | exercise: _parseInt(json['exercise']), |
-
Please register or login to post a comment