Commit 5103946971d087d3323dc35f23ff611ea066690b

Authored by 常守达
1 parent 1aace189

feat(friend): 好友主页

@@ -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,