Showing
15 changed files
with
273 additions
and
112 deletions
| @@ -100,7 +100,7 @@ EXTERNAL SOURCES: | @@ -100,7 +100,7 @@ EXTERNAL SOURCES: | ||
| 100 | 100 | ||
| 101 | SPEC CHECKSUMS: | 101 | SPEC CHECKSUMS: |
| 102 | DeviceGuru: f0f2bd81953d82777689f22025bca418e0c51d64 | 102 | DeviceGuru: f0f2bd81953d82777689f22025bca418e0c51d64 |
| 103 | - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | 103 | + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 |
| 104 | fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 | 104 | fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 |
| 105 | image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537 | 105 | image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537 |
| 106 | image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | 106 | image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a |
| 1 | +import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; | ||
| 2 | +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'; | ||
| 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'; | ||
| 7 | +import 'package:get/get.dart'; | ||
| 8 | + | ||
| 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 { | ||
| 23 | + @override | ||
| 24 | + void dependencies() { | ||
| 25 | + final args = Get.arguments as FriendHomeArguments; | ||
| 26 | + Get.lazyPut<TodayController>( | ||
| 27 | + () => TodayController( | ||
| 28 | + Get.find<UserApi>(), | ||
| 29 | + Get.find<HealthApi>(), | ||
| 30 | + Get.find<UserStateService>(), | ||
| 31 | + Get.find<HealthKitUploadService>(), | ||
| 32 | + Get.find<AppleHealthUploadTool>(), | ||
| 33 | + friendUserId: args.userId, // 好友 userId,驱动 isFriend = true | ||
| 34 | + ), | ||
| 35 | + tag: 'friend_${args.userId}', // 动态 tag,支持多个好友同时在路由栈中 | ||
| 36 | + ); | ||
| 37 | + } | ||
| 38 | +} |
| 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'; | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:get/get.dart'; | ||
| 5 | + | ||
| 6 | +import '../bindings/friend_home_binding.dart'; | ||
| 7 | + | ||
| 8 | +class FriendHomePage extends GetView<TodayController> { | ||
| 9 | + const FriendHomePage({super.key}); | ||
| 10 | + | ||
| 11 | + FriendHomeArguments get _args => Get.arguments as FriendHomeArguments; | ||
| 12 | + | ||
| 13 | + /// 与 FriendHomeBinding 中注册的 tag 保持一致 | ||
| 14 | + @override | ||
| 15 | + String? get tag => 'friend_${_args.userId}'; | ||
| 16 | + | ||
| 17 | + @override | ||
| 18 | + Widget build(BuildContext context) { | ||
| 19 | + return Scaffold( | ||
| 20 | + body: TodayTabBody(controller: controller), | ||
| 21 | + ); | ||
| 22 | + } | ||
| 23 | +} |
| @@ -12,7 +12,6 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | @@ -12,7 +12,6 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | ||
| 12 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; | 12 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; |
| 13 | import 'package:doublefeel_flutter/data/models/health/health_models.dart'; | 13 | import 'package:doublefeel_flutter/data/models/health/health_models.dart'; |
| 14 | import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart'; | 14 | import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart'; |
| 15 | -import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | ||
| 16 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; | 15 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; |
| 17 | import 'package:flutter/material.dart'; | 16 | import 'package:flutter/material.dart'; |
| 18 | import 'package:get/get.dart'; | 17 | import 'package:get/get.dart'; |
| @@ -46,8 +45,9 @@ class TodayController extends GetxController { | @@ -46,8 +45,9 @@ class TodayController extends GetxController { | ||
| 46 | this._healthApi, | 45 | this._healthApi, |
| 47 | this._userStateService, | 46 | this._userStateService, |
| 48 | this._healthKitUploadService, | 47 | this._healthKitUploadService, |
| 49 | - this._appleHealthUploadTool, | ||
| 50 | - ); | 48 | + this._appleHealthUploadTool, { |
| 49 | + this.friendUserId, // null = 自己,非 null = 好友 | ||
| 50 | + }); | ||
| 51 | 51 | ||
| 52 | final UserApi _userApi; | 52 | final UserApi _userApi; |
| 53 | final HealthApi _healthApi; | 53 | final HealthApi _healthApi; |
| @@ -56,6 +56,12 @@ class TodayController extends GetxController { | @@ -56,6 +56,12 @@ class TodayController extends GetxController { | ||
| 56 | final AppleHealthUploadTool _appleHealthUploadTool; | 56 | final AppleHealthUploadTool _appleHealthUploadTool; |
| 57 | final HealthKitHostApi _hostApi = HealthKitHostApi(); | 57 | final HealthKitHostApi _hostApi = HealthKitHostApi(); |
| 58 | 58 | ||
| 59 | + /// 好友的 userId;null 表示查看自己的数据,非 null 表示查看好友的数据。 | ||
| 60 | + final int? friendUserId; | ||
| 61 | + | ||
| 62 | + /// 是否正在查看好友数据。 | ||
| 63 | + bool get isFriend => friendUserId != null; | ||
| 64 | + | ||
| 59 | UserStateService get userStateService => _userStateService; | 65 | UserStateService get userStateService => _userStateService; |
| 60 | 66 | ||
| 61 | late final DateTime firstSelectableDay; | 67 | late final DateTime firstSelectableDay; |
| @@ -164,7 +170,6 @@ class TodayController extends GetxController { | @@ -164,7 +170,6 @@ class TodayController extends GetxController { | ||
| 164 | isLoadingToday.value = true; | 170 | isLoadingToday.value = true; |
| 165 | try { | 171 | try { |
| 166 | await Future.wait([ | 172 | await Future.wait([ |
| 167 | - _refreshUserGreeting(), | ||
| 168 | _refreshHealthAuthorizationState(), | 173 | _refreshHealthAuthorizationState(), |
| 169 | _refreshHealthDataForDate(date), | 174 | _refreshHealthDataForDate(date), |
| 170 | ]); | 175 | ]); |
| @@ -195,16 +200,6 @@ class TodayController extends GetxController { | @@ -195,16 +200,6 @@ class TodayController extends GetxController { | ||
| 195 | } | 200 | } |
| 196 | } | 201 | } |
| 197 | 202 | ||
| 198 | - Future<void> _refreshUserGreeting() async { | ||
| 199 | - final result = await _userApi.getUserInfo(errorHandlingPolicy: null); | ||
| 200 | - if (result case AppSuccess<UserInfoResponse>(data: final user)) { | ||
| 201 | - final name = user.nickname?.trim(); | ||
| 202 | - stressSubtitle.value = name == null || name.isEmpty | ||
| 203 | - ? 'Hi, 你今日的综合压力状态' | ||
| 204 | - : 'Hi, $name 今日的综合压力状态'; | ||
| 205 | - } | ||
| 206 | - } | ||
| 207 | - | ||
| 208 | Future<void> _refreshHealthAuthorizationState() async { | 203 | Future<void> _refreshHealthAuthorizationState() async { |
| 209 | try { | 204 | try { |
| 210 | final result = await _hostApi.checkHealthAppAuthorization(); | 205 | final result = await _hostApi.checkHealthAppAuthorization(); |
| @@ -224,34 +219,37 @@ class TodayController extends GetxController { | @@ -224,34 +219,37 @@ class TodayController extends GetxController { | ||
| 224 | 219 | ||
| 225 | final todayResultFuture = isToday | 220 | final todayResultFuture = isToday |
| 226 | ? _healthApi.getTodayData( | 221 | ? _healthApi.getTodayData( |
| 227 | - isOther: false, | 222 | + isOther: isFriend, |
| 228 | errorHandlingPolicy: null, | 223 | errorHandlingPolicy: null, |
| 229 | ) | 224 | ) |
| 230 | : Future.value( | 225 | : Future.value( |
| 231 | AppFailure<TodayStatusData>(AppUnknownError('Not today'))); | 226 | AppFailure<TodayStatusData>(AppUnknownError('Not today'))); |
| 232 | 227 | ||
| 233 | - final latestHrvResultFuture = isToday | 228 | + final latestHrvResultFuture = isToday && !isFriend |
| 234 | ? _healthApi.getLatestHrvData( | 229 | ? _healthApi.getLatestHrvData( |
| 235 | errorHandlingPolicy: null, | 230 | errorHandlingPolicy: null, |
| 236 | ) | 231 | ) |
| 237 | : Future.value(AppFailure<LatestHrvData>(AppUnknownError('Not today'))); | 232 | : Future.value(AppFailure<LatestHrvData>(AppUnknownError('Not today'))); |
| 238 | 233 | ||
| 239 | final hrvStatisticsResultFuture = _healthApi.getHrvStatistics( | 234 | final hrvStatisticsResultFuture = _healthApi.getHrvStatistics( |
| 240 | - isOther: false, | 235 | + isOther: isFriend, |
| 241 | dateRangeType: dateRangeType, | 236 | dateRangeType: dateRangeType, |
| 242 | startDate: startDate, | 237 | startDate: startDate, |
| 238 | + targetUserId: friendUserId, | ||
| 243 | errorHandlingPolicy: null, | 239 | errorHandlingPolicy: null, |
| 244 | ); | 240 | ); |
| 245 | final sleepStatisticsResultFuture = _healthApi.getSleepStateStatistics( | 241 | final sleepStatisticsResultFuture = _healthApi.getSleepStateStatistics( |
| 246 | - isOther: false, | 242 | + isOther: isFriend, |
| 247 | dateRangeType: dateRangeType, | 243 | dateRangeType: dateRangeType, |
| 248 | startDate: startDate, | 244 | startDate: startDate, |
| 245 | + targetUserId: friendUserId, | ||
| 249 | errorHandlingPolicy: null, | 246 | errorHandlingPolicy: null, |
| 250 | ); | 247 | ); |
| 251 | final activityStatisticsResultFuture = _healthApi.getActivityBurnStatistics( | 248 | final activityStatisticsResultFuture = _healthApi.getActivityBurnStatistics( |
| 252 | - isOther: false, | 249 | + isOther: isFriend, |
| 253 | dateRangeType: dateRangeType, | 250 | dateRangeType: dateRangeType, |
| 254 | startDate: startDate, | 251 | startDate: startDate, |
| 252 | + targetUserId: friendUserId, | ||
| 255 | errorHandlingPolicy: null, | 253 | errorHandlingPolicy: null, |
| 256 | ); | 254 | ); |
| 257 | 255 |
| 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'; | ||
| 4 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | 5 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; |
| 5 | import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_view.dart'; | 6 | import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_view.dart'; |
| 6 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; | 7 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; |
| @@ -68,12 +69,12 @@ class MyTab extends GetView<MyController> { | @@ -68,12 +69,12 @@ class MyTab extends GetView<MyController> { | ||
| 68 | onTap: () => Get.toNamed(Routes.HELP), | 69 | onTap: () => Get.toNamed(Routes.HELP), |
| 69 | ), | 70 | ), |
| 70 | const SizedBox(height: 12), | 71 | const SizedBox(height: 12), |
| 71 | - _SettingsRow( | 72 | + _SettingsRow( |
| 72 | title: 'Apple Health Upload 测试', | 73 | title: 'Apple Health Upload 测试', |
| 73 | - onTap: () { | 74 | + onTap: () { |
| 74 | controller.testAppleHealthUpload(); | 75 | controller.testAppleHealthUpload(); |
| 75 | - }, | ||
| 76 | - ), | 76 | + }, |
| 77 | + ), | ||
| 77 | if (kDebugMode) ...[ | 78 | if (kDebugMode) ...[ |
| 78 | const SizedBox(height: 12), | 79 | const SizedBox(height: 12), |
| 79 | _SettingsRow( | 80 | _SettingsRow( |
| 1 | +import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measurement_bottom_sheet.dart'; | ||
| 1 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; | 2 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; |
| 2 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; | 3 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; |
| 3 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 4 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| @@ -9,7 +10,6 @@ import 'package:intl/intl.dart'; | @@ -9,7 +10,6 @@ import 'package:intl/intl.dart'; | ||
| 9 | import 'package:table_calendar/table_calendar.dart'; | 10 | import 'package:table_calendar/table_calendar.dart'; |
| 10 | 11 | ||
| 11 | import '../../controllers/today_controller.dart'; | 12 | import '../../controllers/today_controller.dart'; |
| 12 | -import '../../widgets/today/hrv_measurement_bottom_sheet.dart'; | ||
| 13 | import '../../widgets/today/today_health_data_auth_card.dart'; | 13 | import '../../widgets/today/today_health_data_auth_card.dart'; |
| 14 | import '../../widgets/today/today_hrv_ad_banner.dart'; | 14 | import '../../widgets/today/today_hrv_ad_banner.dart'; |
| 15 | import '../../widgets/today/today_hrv_chart_card.dart'; | 15 | import '../../widgets/today/today_hrv_chart_card.dart'; |
| @@ -23,6 +23,15 @@ import '../../widgets/today/stress_progress_bar.dart'; | @@ -23,6 +23,15 @@ import '../../widgets/today/stress_progress_bar.dart'; | ||
| 23 | class TodayTab extends GetView<TodayController> { | 23 | class TodayTab extends GetView<TodayController> { |
| 24 | const TodayTab({super.key}); | 24 | const TodayTab({super.key}); |
| 25 | 25 | ||
| 26 | + @override | ||
| 27 | + Widget build(BuildContext context) => TodayTabBody(controller: controller); | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +class TodayTabBody extends StatelessWidget { | ||
| 31 | + const TodayTabBody({super.key, required this.controller}); | ||
| 32 | + | ||
| 33 | + final TodayController controller; | ||
| 34 | + | ||
| 26 | final _bgColor = const Color(0xFFF2F2F7); | 35 | final _bgColor = const Color(0xFFF2F2F7); |
| 27 | final _weekCalendarHeight = 58.0; | 36 | final _weekCalendarHeight = 58.0; |
| 28 | final _topContentHeight = /*_topBarHeight + _weekCalendarHeight*/ 106.0; | 37 | final _topContentHeight = /*_topBarHeight + _weekCalendarHeight*/ 106.0; |
| @@ -183,13 +192,13 @@ class TodayTab extends GetView<TodayController> { | @@ -183,13 +192,13 @@ class TodayTab extends GetView<TodayController> { | ||
| 183 | ? const SizedBox.shrink() | 192 | ? const SizedBox.shrink() |
| 184 | : const PremiumCard(); | 193 | : const PremiumCard(); |
| 185 | }), | 194 | }), |
| 186 | - const TodayHrvAdBanner(), | ||
| 187 | - const TodayPartnerAdBanner(), | ||
| 188 | - const TodayHrvNumberCard(), | 195 | + TodayHrvAdBanner(controller: controller), |
| 196 | + TodayPartnerAdBanner(controller: controller), | ||
| 197 | + TodayHrvNumberCard(controller: controller), | ||
| 189 | const SizedBox(height: 12), | 198 | const SizedBox(height: 12), |
| 190 | - const TodayHrvChartCard(), | 199 | + TodayHrvChartCard(controller: controller), |
| 191 | const SizedBox(height: 12), | 200 | const SizedBox(height: 12), |
| 192 | - const _LatestHrvCard(), | 201 | + _LatestHrvCard(controller: controller), |
| 193 | const SizedBox(height: 12), | 202 | const SizedBox(height: 12), |
| 194 | measureHRVButton(context), | 203 | measureHRVButton(context), |
| 195 | Padding( | 204 | Padding( |
| @@ -203,8 +212,8 @@ class TodayTab extends GetView<TodayController> { | @@ -203,8 +212,8 @@ class TodayTab extends GetView<TodayController> { | ||
| 203 | ), | 212 | ), |
| 204 | ), | 213 | ), |
| 205 | ), | 214 | ), |
| 206 | - const TodaySleepCard(), | ||
| 207 | - const TodayActivityCard(), | 215 | + TodaySleepCard(controller: controller), |
| 216 | + TodayActivityCard(controller: controller), | ||
| 208 | Container( | 217 | Container( |
| 209 | alignment: Alignment.center, | 218 | alignment: Alignment.center, |
| 210 | margin: EdgeInsets.only( | 219 | margin: EdgeInsets.only( |
| @@ -458,8 +467,10 @@ class TodayTab extends GetView<TodayController> { | @@ -458,8 +467,10 @@ class TodayTab extends GetView<TodayController> { | ||
| 458 | } | 467 | } |
| 459 | } | 468 | } |
| 460 | 469 | ||
| 461 | -class _LatestHrvCard extends GetView<TodayController> { | ||
| 462 | - const _LatestHrvCard(); | 470 | +class _LatestHrvCard extends StatelessWidget { |
| 471 | + const _LatestHrvCard({required this.controller}); | ||
| 472 | + | ||
| 473 | + final TodayController controller; | ||
| 463 | 474 | ||
| 464 | @override | 475 | @override |
| 465 | Widget build(BuildContext context) { | 476 | Widget build(BuildContext context) { |
| @@ -6,8 +6,10 @@ import 'package:get/get.dart'; | @@ -6,8 +6,10 @@ import 'package:get/get.dart'; | ||
| 6 | 6 | ||
| 7 | import '../../controllers/today_controller.dart'; | 7 | import '../../controllers/today_controller.dart'; |
| 8 | 8 | ||
| 9 | -class TodayHrvAdBanner extends GetView<TodayController> { | ||
| 10 | - const TodayHrvAdBanner({super.key}); | 9 | +class TodayHrvAdBanner extends StatelessWidget { |
| 10 | + const TodayHrvAdBanner({super.key, required this.controller}); | ||
| 11 | + | ||
| 12 | + final TodayController controller; | ||
| 11 | 13 | ||
| 12 | @override | 14 | @override |
| 13 | Widget build(BuildContext context) { | 15 | Widget build(BuildContext context) { |
| @@ -37,8 +39,10 @@ class TodayHrvAdBanner extends GetView<TodayController> { | @@ -37,8 +39,10 @@ class TodayHrvAdBanner extends GetView<TodayController> { | ||
| 37 | } | 39 | } |
| 38 | 40 | ||
| 39 | /// Figma: 添加亲密联系人 Banner,右上角关闭按钮 | 41 | /// Figma: 添加亲密联系人 Banner,右上角关闭按钮 |
| 40 | -class TodayPartnerAdBanner extends GetView<TodayController> { | ||
| 41 | - const TodayPartnerAdBanner({super.key}); | 42 | +class TodayPartnerAdBanner extends StatelessWidget { |
| 43 | + const TodayPartnerAdBanner({super.key, required this.controller}); | ||
| 44 | + | ||
| 45 | + final TodayController controller; | ||
| 42 | 46 | ||
| 43 | @override | 47 | @override |
| 44 | Widget build(BuildContext context) { | 48 | Widget build(BuildContext context) { |
| @@ -17,8 +17,10 @@ import 'realtime_stress_explanation_bottom_sheet.dart'; | @@ -17,8 +17,10 @@ import 'realtime_stress_explanation_bottom_sheet.dart'; | ||
| 17 | // Y轴: | 17 | // Y轴: |
| 18 | // 范围0ms ~ 80ms | 18 | // 范围0ms ~ 80ms |
| 19 | // 每10ms为一个刻度 | 19 | // 每10ms为一个刻度 |
| 20 | -class TodayHrvChartCard extends GetView<TodayController> { | ||
| 21 | - const TodayHrvChartCard({super.key}); | 20 | +class TodayHrvChartCard extends StatelessWidget { |
| 21 | + const TodayHrvChartCard({super.key, required this.controller}); | ||
| 22 | + | ||
| 23 | + final TodayController controller; | ||
| 22 | 24 | ||
| 23 | static const _h5 = Color(0xFFCCCCCC); | 25 | static const _h5 = Color(0xFFCCCCCC); |
| 24 | 26 |
| @@ -6,8 +6,10 @@ import 'package:get/get.dart'; | @@ -6,8 +6,10 @@ import 'package:get/get.dart'; | ||
| 6 | import '../../controllers/today_controller.dart'; | 6 | import '../../controllers/today_controller.dart'; |
| 7 | 7 | ||
| 8 | /// Figma: 两列数字展示 — 该日平均HRV(46ms) + 静息心率(63bpm) | 8 | /// Figma: 两列数字展示 — 该日平均HRV(46ms) + 静息心率(63bpm) |
| 9 | -class TodayHrvNumberCard extends GetView<TodayController> { | ||
| 10 | - const TodayHrvNumberCard({super.key}); | 9 | +class TodayHrvNumberCard extends StatelessWidget { |
| 10 | + const TodayHrvNumberCard({super.key, required this.controller}); | ||
| 11 | + | ||
| 12 | + final TodayController controller; | ||
| 11 | 13 | ||
| 12 | @override | 14 | @override |
| 13 | Widget build(BuildContext context) { | 15 | Widget build(BuildContext context) { |
| @@ -6,8 +6,10 @@ import 'package:get/get.dart'; | @@ -6,8 +6,10 @@ import 'package:get/get.dart'; | ||
| 6 | 6 | ||
| 7 | import '../../controllers/today_controller.dart'; | 7 | import '../../controllers/today_controller.dart'; |
| 8 | 8 | ||
| 9 | -class TodaySleepCard extends GetView<TodayController> { | ||
| 10 | - const TodaySleepCard({super.key}); | 9 | +class TodaySleepCard extends StatelessWidget { |
| 10 | + const TodaySleepCard({super.key, required this.controller}); | ||
| 11 | + | ||
| 12 | + final TodayController controller; | ||
| 11 | 13 | ||
| 12 | @override | 14 | @override |
| 13 | Widget build(BuildContext context) { | 15 | Widget build(BuildContext context) { |
| @@ -82,8 +84,10 @@ class TodaySleepCard extends GetView<TodayController> { | @@ -82,8 +84,10 @@ class TodaySleepCard extends GetView<TodayController> { | ||
| 82 | } | 84 | } |
| 83 | } | 85 | } |
| 84 | 86 | ||
| 85 | -class TodayActivityCard extends GetView<TodayController> { | ||
| 86 | - const TodayActivityCard({super.key}); | 87 | +class TodayActivityCard extends StatelessWidget { |
| 88 | + const TodayActivityCard({super.key, required this.controller}); | ||
| 89 | + | ||
| 90 | + final TodayController controller; | ||
| 87 | 91 | ||
| 88 | @override | 92 | @override |
| 89 | Widget build(BuildContext context) { | 93 | Widget build(BuildContext context) { |
| @@ -38,6 +38,8 @@ class LoginController extends GetxController { | @@ -38,6 +38,8 @@ class LoginController extends GetxController { | ||
| 38 | 38 | ||
| 39 | final termsChecked = false.obs; | 39 | final termsChecked = false.obs; |
| 40 | 40 | ||
| 41 | + AppleSignInModel? appleSignInModel; | ||
| 42 | + | ||
| 41 | void toggleTermsChecked() { | 43 | void toggleTermsChecked() { |
| 42 | termsChecked.value = !termsChecked.value; | 44 | termsChecked.value = !termsChecked.value; |
| 43 | } | 45 | } |
| @@ -56,10 +58,13 @@ class LoginController extends GetxController { | @@ -56,10 +58,13 @@ class LoginController extends GetxController { | ||
| 56 | return; | 58 | return; |
| 57 | } | 59 | } |
| 58 | final result = await PlatformHostApi().requestAppleSignIn(); | 60 | final result = await PlatformHostApi().requestAppleSignIn(); |
| 59 | - if (result == null) { | 61 | + // result 为 null 或 identityToken 为空,说明用户取消或苹果授权失败 |
| 62 | + if (result == null || result.identityToken?.isNotEmpty != true) { | ||
| 63 | + appleSignInModel = null; | ||
| 60 | return; | 64 | return; |
| 61 | } | 65 | } |
| 62 | - | 66 | + appleSignInModel = result; |
| 67 | + await _loginWithApple(result); | ||
| 63 | } | 68 | } |
| 64 | 69 | ||
| 65 | void onDebugPressed() { | 70 | void onDebugPressed() { |
| @@ -164,80 +169,102 @@ class LoginController extends GetxController { | @@ -164,80 +169,102 @@ class LoginController extends GetxController { | ||
| 164 | ); | 169 | ); |
| 165 | 170 | ||
| 166 | if (loginRes is AppSuccess<LoginResponse>) { | 171 | if (loginRes is AppSuccess<LoginResponse>) { |
| 167 | - final loginData = loginRes.data; | ||
| 168 | - final isRegister = loginData.isNewUser == true; | ||
| 169 | - String accessToken = loginData.accessToken; | ||
| 170 | - | ||
| 171 | - if (isRegister) { | ||
| 172 | - final regRes = await _userApi.register( | ||
| 173 | - telephone: cleanPhone, | ||
| 174 | - verifyCode: codeInput.value, | ||
| 175 | - ); | ||
| 176 | - if (regRes is AppSuccess<RegisterResponse>) { | ||
| 177 | - accessToken = regRes.data.accessToken; | ||
| 178 | - } else { | ||
| 179 | - isLoggingIn.value = false; | ||
| 180 | - return; | ||
| 181 | - } | 172 | + await _handleLoginSuccess(loginRes.data); |
| 173 | + } else { | ||
| 174 | + isLoggingIn.value = false; | ||
| 175 | + } | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + Future<void> _loginWithApple(AppleSignInModel appleModel) async { | ||
| 179 | + isLoggingIn.value = true; | ||
| 180 | + final loginRes = await _userApi.loginWithApple( | ||
| 181 | + identityToken: appleModel.identityToken!, | ||
| 182 | + email: appleModel.email, | ||
| 183 | + nickname: appleModel.nickname, | ||
| 184 | + ); | ||
| 185 | + | ||
| 186 | + if (loginRes is AppSuccess<LoginResponse>) { | ||
| 187 | + await _handleLoginSuccess(loginRes.data, isApple: true); | ||
| 188 | + } else { | ||
| 189 | + isLoggingIn.value = false; | ||
| 190 | + } | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + /// 登录/注册成功后的公共处理逻辑(手机号登录与苹果登录共用) | ||
| 194 | + Future<void> _handleLoginSuccess( | ||
| 195 | + LoginResponse loginData, { | ||
| 196 | + bool isApple = false, | ||
| 197 | + }) async { | ||
| 198 | + final isRegister = loginData.isNewUser == true; | ||
| 199 | + String accessToken = loginData.accessToken; | ||
| 200 | + | ||
| 201 | + if (isRegister && !isApple) { | ||
| 202 | + // 苹果登录时后端直接完成注册,无需二次注册请求 | ||
| 203 | + final regRes = await _userApi.register( | ||
| 204 | + telephone: cleanPhone, | ||
| 205 | + verifyCode: codeInput.value, | ||
| 206 | + ); | ||
| 207 | + if (regRes is AppSuccess<RegisterResponse>) { | ||
| 208 | + accessToken = regRes.data.accessToken; | ||
| 209 | + } else { | ||
| 210 | + isLoggingIn.value = false; | ||
| 211 | + return; | ||
| 182 | } | 212 | } |
| 213 | + } | ||
| 183 | 214 | ||
| 184 | - // Fetch user info and VIP info | ||
| 185 | - final userInfoResFuture = _userApi.getUserInfo(accessToken: accessToken); | ||
| 186 | - final vipInfoResFuture = isRegister | ||
| 187 | - ? Future.value(null) | ||
| 188 | - : _vipApi.getVipInfo(accessToken: accessToken); | ||
| 189 | - | ||
| 190 | - final results = await Future.wait([userInfoResFuture, vipInfoResFuture]); | ||
| 191 | - final userInfoRes = results[0]; | ||
| 192 | - final vipInfoRes = results[1]; | ||
| 193 | - | ||
| 194 | - if (userInfoRes is AppSuccess<UserInfoResponse>) { | ||
| 195 | - final me = userInfoRes.data; | ||
| 196 | - UserInfoResponse? partner; | ||
| 197 | - | ||
| 198 | - if ((me.pairId ?? 0) > 0) { | ||
| 199 | - final partnerRes = | ||
| 200 | - await _userApi.getPartnerUserInfo(accessToken: accessToken); | ||
| 201 | - if (partnerRes is AppSuccess<BoundUserInfoResponse>) { | ||
| 202 | - partner = partnerRes.data.partnerUserInfo; | ||
| 203 | - } | 215 | + // Fetch user info and VIP info |
| 216 | + final userInfoResFuture = _userApi.getUserInfo(accessToken: accessToken); | ||
| 217 | + final vipInfoResFuture = isRegister | ||
| 218 | + ? Future.value(null) | ||
| 219 | + : _vipApi.getVipInfo(accessToken: accessToken); | ||
| 220 | + | ||
| 221 | + final results = await Future.wait([userInfoResFuture, vipInfoResFuture]); | ||
| 222 | + final userInfoRes = results[0]; | ||
| 223 | + final vipInfoRes = results[1]; | ||
| 224 | + | ||
| 225 | + if (userInfoRes is AppSuccess<UserInfoResponse>) { | ||
| 226 | + final me = userInfoRes.data; | ||
| 227 | + UserInfoResponse? partner; | ||
| 228 | + | ||
| 229 | + if ((me.pairId ?? 0) > 0) { | ||
| 230 | + final partnerRes = | ||
| 231 | + await _userApi.getPartnerUserInfo(accessToken: accessToken); | ||
| 232 | + if (partnerRes is AppSuccess<BoundUserInfoResponse>) { | ||
| 233 | + partner = partnerRes.data.partnerUserInfo; | ||
| 204 | } | 234 | } |
| 235 | + } | ||
| 205 | 236 | ||
| 206 | - VipInfo? vip; | ||
| 207 | - if (vipInfoRes is AppSuccess<VipInfo>) { | ||
| 208 | - vip = vipInfoRes.data; | ||
| 209 | - } | 237 | + VipInfo? vip; |
| 238 | + if (vipInfoRes is AppSuccess<VipInfo>) { | ||
| 239 | + vip = vipInfoRes.data; | ||
| 240 | + } | ||
| 210 | 241 | ||
| 211 | - await _userPrefs.updateFromLogin( | ||
| 212 | - accessToken: accessToken, | ||
| 213 | - me: me, | ||
| 214 | - partner: partner, | ||
| 215 | - vip: vip, | ||
| 216 | - ); | 242 | + await _userPrefs.updateFromLogin( |
| 243 | + accessToken: accessToken, | ||
| 244 | + me: me, | ||
| 245 | + partner: partner, | ||
| 246 | + vip: vip, | ||
| 247 | + ); | ||
| 217 | 248 | ||
| 218 | - await _userStateService.onLogin(); | 249 | + await _userStateService.onLogin(); |
| 219 | 250 | ||
| 220 | - // 用户登录成功即代表同意了协议,持久化到本地供冷启动时 SDK 初始化判断使用 | ||
| 221 | - await Get.find<LocalStorage>().setTermsAgreed(true); | 251 | + // 用户登录成功即代表同意了协议,持久化到本地供冷启动时 SDK 初始化判断使用 |
| 252 | + await Get.find<LocalStorage>().setTermsAgreed(true); | ||
| 222 | 253 | ||
| 223 | - isLoggingIn.value = false; | 254 | + isLoggingIn.value = false; |
| 224 | 255 | ||
| 225 | - // 根据引导完成状态决定跳转目标 | ||
| 226 | - final userId = me.id ?? 0; | ||
| 227 | - if (_userAccount.hasCompletedOnboarding(userId)) { | ||
| 228 | - // 该账号已完成引导,直接进主页 | ||
| 229 | - Get.offAllNamed(AppRoutes.home); | ||
| 230 | - } else { | ||
| 231 | - // 新用户或未完成引导,进入引导页(支持断点续做) | ||
| 232 | - final resumeStage = _userAccount.onboardingResumeStage(userId); | ||
| 233 | - Get.offAllNamed( | ||
| 234 | - AppRoutes.userOnboarding, | ||
| 235 | - arguments: | ||
| 236 | - resumeStage != null ? {'resumeStage': resumeStage} : null, | ||
| 237 | - ); | ||
| 238 | - } | 256 | + // 根据引导完成状态决定跳转目标 |
| 257 | + final userId = me.id ?? 0; | ||
| 258 | + if (_userAccount.hasCompletedOnboarding(userId)) { | ||
| 259 | + // 该账号已完成引导,直接进主页 | ||
| 260 | + Get.offAllNamed(AppRoutes.home); | ||
| 239 | } else { | 261 | } else { |
| 240 | - isLoggingIn.value = false; | 262 | + // 新用户或未完成引导,进入引导页(支持断点续做) |
| 263 | + final resumeStage = _userAccount.onboardingResumeStage(userId); | ||
| 264 | + Get.offAllNamed( | ||
| 265 | + AppRoutes.userOnboarding, | ||
| 266 | + arguments: resumeStage != null ? {'resumeStage': resumeStage} : null, | ||
| 267 | + ); | ||
| 241 | } | 268 | } |
| 242 | } else { | 269 | } else { |
| 243 | isLoggingIn.value = false; | 270 | isLoggingIn.value = false; |
| 1 | +import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.dart'; | ||
| 1 | import 'package:flutter/foundation.dart'; | 3 | import 'package:flutter/foundation.dart'; |
| 2 | 4 | ||
| 3 | import 'package:get/get.dart'; | 5 | import 'package:get/get.dart'; |
| @@ -103,6 +105,11 @@ abstract final class AppPages { | @@ -103,6 +105,11 @@ abstract final class AppPages { | ||
| 103 | binding: BindPartnerBinding(), | 105 | binding: BindPartnerBinding(), |
| 104 | ), | 106 | ), |
| 105 | GetPage( | 107 | GetPage( |
| 108 | + name: Routes.FRIEND_HOME, | ||
| 109 | + page: () => const FriendHomePage(), | ||
| 110 | + binding: FriendHomeBinding(), | ||
| 111 | + ), | ||
| 112 | + GetPage( | ||
| 106 | name: Routes.ADD_FRIEND, | 113 | name: Routes.ADD_FRIEND, |
| 107 | page: () => const AddFriendView(), | 114 | page: () => const AddFriendView(), |
| 108 | binding: AddFriendBinding(), | 115 | binding: AddFriendBinding(), |
| @@ -19,6 +19,7 @@ abstract class Routes { | @@ -19,6 +19,7 @@ abstract class Routes { | ||
| 19 | static const WATCH_THEME_CUSTOM_PREVIEW = _Paths.WATCH_THEME_CUSTOM_PREVIEW; | 19 | static const WATCH_THEME_CUSTOM_PREVIEW = _Paths.WATCH_THEME_CUSTOM_PREVIEW; |
| 20 | static const ACCOUNT_SETTINGS = _Paths.ACCOUNT_SETTINGS; | 20 | static const ACCOUNT_SETTINGS = _Paths.ACCOUNT_SETTINGS; |
| 21 | static const FRIEND_TREND = _Paths.FRIEND_TREND; | 21 | static const FRIEND_TREND = _Paths.FRIEND_TREND; |
| 22 | + static const FRIEND_HOME = _Paths.FRIEND_HOME; | ||
| 22 | static const APPLE_HEALTH_UPLOAD_TEST = _Paths.APPLE_HEALTH_UPLOAD_TEST; | 23 | static const APPLE_HEALTH_UPLOAD_TEST = _Paths.APPLE_HEALTH_UPLOAD_TEST; |
| 23 | } | 24 | } |
| 24 | 25 | ||
| @@ -40,5 +41,6 @@ abstract class _Paths { | @@ -40,5 +41,6 @@ abstract class _Paths { | ||
| 40 | static const WATCH_THEME_CUSTOM_PREVIEW = '/watch-theme/custom-preview'; | 41 | static const WATCH_THEME_CUSTOM_PREVIEW = '/watch-theme/custom-preview'; |
| 41 | static const ACCOUNT_SETTINGS = '/account-settings'; | 42 | static const ACCOUNT_SETTINGS = '/account-settings'; |
| 42 | static const FRIEND_TREND = '/friend-trend'; | 43 | static const FRIEND_TREND = '/friend-trend'; |
| 44 | + static const FRIEND_HOME = '/friend-home'; | ||
| 43 | static const APPLE_HEALTH_UPLOAD_TEST = '/apple-health-upload-test'; | 45 | static const APPLE_HEALTH_UPLOAD_TEST = '/apple-health-upload-test'; |
| 44 | } | 46 | } |
| @@ -42,6 +42,27 @@ class UserApi { | @@ -42,6 +42,27 @@ class UserApi { | ||
| 42 | ); | 42 | ); |
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | + Future<AppResult<LoginResponse>> loginWithApple({ | ||
| 46 | + required String identityToken, | ||
| 47 | + String? email, | ||
| 48 | + String? nickname, | ||
| 49 | + }) { | ||
| 50 | + return safeCall( | ||
| 51 | + call: () async { | ||
| 52 | + final response = await _dioClient.dio.post( | ||
| 53 | + ApiPaths.userLogin, | ||
| 54 | + data: AppleLoginRequest( | ||
| 55 | + identityToken: identityToken, | ||
| 56 | + email: email, | ||
| 57 | + nickname: nickname, | ||
| 58 | + ).toJson(), | ||
| 59 | + options: _dioClient.noTokenOptions(), | ||
| 60 | + ); | ||
| 61 | + return LoginResponse.fromJson(response.data as Map<String, dynamic>); | ||
| 62 | + }, | ||
| 63 | + ); | ||
| 64 | + } | ||
| 65 | + | ||
| 45 | Future<AppResult<RegisterResponse>> register({ | 66 | Future<AppResult<RegisterResponse>> register({ |
| 46 | required String telephone, | 67 | required String telephone, |
| 47 | required String verifyCode, | 68 | required String verifyCode, |
| @@ -18,6 +18,27 @@ class LoginRequest { | @@ -18,6 +18,27 @@ class LoginRequest { | ||
| 18 | }; | 18 | }; |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | +class AppleLoginRequest { | ||
| 22 | + const AppleLoginRequest({ | ||
| 23 | + this.loginType = 'apple', | ||
| 24 | + required this.identityToken, | ||
| 25 | + this.email, | ||
| 26 | + this.nickname, | ||
| 27 | + }); | ||
| 28 | + | ||
| 29 | + final String loginType; | ||
| 30 | + final String identityToken; | ||
| 31 | + final String? email; | ||
| 32 | + final String? nickname; | ||
| 33 | + | ||
| 34 | + Map<String, dynamic> toJson() => { | ||
| 35 | + 'login_type': loginType, | ||
| 36 | + 'identity_token': identityToken, | ||
| 37 | + if (email != null) 'email': email, | ||
| 38 | + if (nickname != null) 'nickname': nickname, | ||
| 39 | + }; | ||
| 40 | +} | ||
| 41 | + | ||
| 21 | class RegisterRequest { | 42 | class RegisterRequest { |
| 22 | const RegisterRequest({ | 43 | const RegisterRequest({ |
| 23 | required this.telephone, | 44 | required this.telephone, |
-
Please register or login to post a comment