Commit 31cbe47bfc816476dfcbd59be901e3cb015125c3

Authored by 常守达
1 parent c62867d9

feat(today): 好友主页

@@ -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',
  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 }
  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']),