Commit bb2c3b2b30e0dd43f7ffb702e7a9824c5de3cc48

Authored by 常守达
1 parent cf74440a

feat(route):路由

... ... @@ -38,8 +38,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
children: [
_buildGradientBackground(),
Container(
padding:
EdgeInsets.only(top: MediaQuery.paddingOf(context).top + 4),
padding: EdgeInsets.only(top: 4),
child: ListView(
physics: const ClampingScrollPhysics(),
children: [
... ...
import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/local_storage.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/scheduler.dart';
import 'package:get/get.dart';
import '../../../routes/app_pages.dart';
import '../../purchase/purchase_route_args.dart';
import '../../app_review_prompt/logic/app_review_prompt_logic.dart';
import '../../report_common/models/report_period.dart';
import 'today_controller.dart';
import 'trend/trend_controller.dart';
class HomeController extends GetxController {
... ... @@ -20,6 +27,7 @@ class HomeController extends GetxController {
);
final LocalStorage _storage = Get.find<LocalStorage>();
final UserStateService userStateService = Get.find<UserStateService>();
final PlatformHostApi _platformHostApi = PlatformHostApi();
/// 当前选中的底部 tab 索引
final selectedIndex = 0.obs;
... ... @@ -29,6 +37,16 @@ class HomeController extends GetxController {
void onInit() {
super.onInit();
_mainChannel.setMethodCallHandler(_handleNativeMethodCall);
SchedulerBinding.instance.addPostFrameCallback((_) async {
try {
final route = await _platformHostApi.requestUnhandedUrl();
// final route ='doublefeel://flutter/home?tab=today&scroll_to_latest_hrv=true';
// final route = 'doublefeel://flutter/friend-home?user_id=131';
// final route = 'doublefeel://flutter/home?tab=trend&type=sleep';
// final route ='doublefeel://flutter/friend-trend?type=sleep&user_id=131';
_checkFlutterRoute(route);
} catch (_) {}
});
}
@override
... ... @@ -37,15 +55,6 @@ class HomeController extends GetxController {
super.onClose();
}
@override
void onReady() async {
final unhandedUrl = await PlatformHostApi().requestUnhandedUrl();
debugPrint(
'[HomeController] requestUnhandedUrl: '
'result: ${unhandedUrl}',
);
}
Future<Object?> _handleNativeMethodCall(MethodCall call) async {
switch (call.method) {
case 'handleFlutterUrl':
... ... @@ -66,7 +75,7 @@ class HomeController extends GetxController {
_ => '',
};
lastFlutterUrl.value = url;
debugPrint('[HomeController.handleFlutterUrl] url: $url');
_checkFlutterRoute(url);
return url.isNotEmpty;
}
... ... @@ -107,4 +116,126 @@ class HomeController extends GetxController {
int _dateKey(DateTime date) =>
date.year * 10000 + date.month * 100 + date.day;
_checkFlutterRoute(String? route) {
try {
if (route == null || route.isEmpty) return;
final uri = Uri.tryParse(route);
if (uri == null) return;
final path = uri.path; // "/home", "/account-settings" 等
final params = uri.queryParameters; // {"tab": "today"} 等
_dispatchRoute(path, params);
} catch (_) {}
}
/// 根据 path + query 参数分派导航动作
void _dispatchRoute(String path, Map<String, String> params) {
switch (path) {
case AppRoutes.home:
_switchHomeTab(params['tab'], params);
break;
default:
// 其他已注册路由 → 跳转页面,query 参数作为 arguments 传入
final isKnown = Get.routeTree.routes.any((r) => r.name == path);
if (!isKnown) return;
if (path == Routes.FRIEND_HOME || path == Routes.FRIEND_TREND) {
final paramUserId = params['user_id'];
if (paramUserId != null) {
final friendUserId = int.tryParse(paramUserId);
if (friendUserId != null) {
final _friendApi = Get.find<FriendApi>();
_friendApi.getFriendInfo(friendUserId).then((result) {
switch (result) {
case AppSuccess(data: final info):
{
if (path == Routes.FRIEND_TREND) {
TrendType? trendType =
getTrendType(params['type'] ?? '');
if (trendType != null) {
Get.toNamed(path,
arguments: FriendTrendArguments(
friendItem: info,
initialTypeIndex: trendType.tabIndex,
));
}
} else {
Get.toNamed(path, arguments: info);
}
}
break;
case AppFailure():
break;
}
});
}
}
} else {
Get.toNamed(path, arguments: params.isNotEmpty ? params : null);
}
}
}
/// 根据 tab 名称切换首页底部 tab
void _switchHomeTab(String? tab, Map<String, String> params) {
switch (tab) {
case 'today':
changeTab(0);
final scrollToLatestHrv =
params['scroll_to_latest_hrv']?.toLowerCase() == 'true';
if (scrollToLatestHrv) {
SchedulerBinding.instance.addPostFrameCallback((_) {
final todayCtrl = Get.find<TodayController>();
if (todayCtrl.scrollController.hasClients) {
todayCtrl.scrollController.animateTo(
850,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
});
}
break;
case 'trend':
changeTab(trendTabIndex);
final type = params['type'];
if (type != null) {
SchedulerBinding.instance.addPostFrameCallback((_) {
TrendType? trendType = getTrendType(type);
if (trendType != null) {
openTrend(
trendType,
);
}
});
}
break;
case 'friends':
changeTab(2);
break;
case 'my':
changeTab(3);
break;
default:
break;
}
}
TrendType? getTrendType(String type) {
switch (type) {
case 'sleep':
return TrendType.sleep;
case 'activity':
return TrendType.activity;
case 'hrv':
return TrendType.hrv;
default:
break;
}
return null;
}
}
... ...
... ... @@ -83,6 +83,7 @@ class TodayController extends GetMaterialController {
final selectedDate = DateTime.now().obs;
final focusedDay = DateTime.now().obs;
final scrollOffset = 0.0.obs;
final scrollController = ScrollController();
final isLoadingToday = false.obs;
final showHealthDataAuthCardStatus = 1.obs;
... ... @@ -158,10 +159,14 @@ class TodayController extends GetMaterialController {
showHrvAdBanner.value = false;
} else {
try {
bool hasInstalledWatchSurface =
await WearEngineHostApi().hasInstalledWatchSurface();
showHrvAdBanner.value = !hasInstalledWatchSurface;
} on Exception catch (e) {}
WearEngineHostApi()
.hasInstalledWatchSurface()
.then((hasInstalledWatchSurface) {
showHrvAdBanner.value = !hasInstalledWatchSurface;
});
} on Exception catch (e) {
AppLogger.e(e);
}
}
}
... ... @@ -228,7 +233,7 @@ class TodayController extends GetMaterialController {
AppLogger.d("requestHealthClientAuthorization : $success");
if (success) {
_performDataUpload();
AppToast.show('刷新完成');
// AppToast.show('刷新完成');
} else {
// Get.to(NoHealthDataPage(
// onRefresh: _performDataUpload,
... ... @@ -597,5 +602,8 @@ class TodayController extends GetMaterialController {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
checkAddFriendVisible();
checkHrvAdBannerVisible();
checkHealthDataAuthCardVisible();
}
}
... ...
... ... @@ -116,6 +116,7 @@ class TodayTabBody extends StatelessWidget {
double pinnedHeaderHeight,
) {
return CustomScrollView(
controller: controller.scrollController,
physics: const ClampingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
... ...
... ... @@ -147,15 +147,26 @@ class _BottomSheetFriendRow extends StatelessWidget {
const SizedBox(width: 8),
// Name
Expanded(
child: Text(
_friendDisplayName(friend),
style: const TextStyle(
color: Color(0xFF0F0F11),
fontSize: 14,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
child: Text.rich(
overflow: TextOverflow.ellipsis,
TextSpan(children: [
TextSpan(
text: friend.remarkName?.trim(),
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
TextSpan(
text: '(${friend.remarkName?.trim()})',
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
])),
),
// Checkbox
Container(
... ... @@ -182,14 +193,4 @@ class _BottomSheetFriendRow extends StatelessWidget {
),
);
}
String _friendDisplayName(FriendItem friend) {
final remark = friend.remarkName?.trim();
if (remark != null && remark.isNotEmpty) return remark;
final nickname = friend.friendNickname?.trim();
if (nickname != null && nickname.isNotEmpty) return nickname;
return l10n.friendsUnknownFriend;
}
}
... ...
... ... @@ -9,6 +9,8 @@ class SplashPage extends GetView<SplashController> {
@override
Widget build(BuildContext context) {
return const Scaffold(
extendBodyBehindAppBar: true,
extendBody: true,
body: Center(
child: CircularProgressIndicator(),
),
... ...
import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:get/get.dart';
... ... @@ -6,8 +7,11 @@ import '../controllers/user_onboarding_controller.dart';
class UserOnboardingBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<UserOnboardingController>(
() => UserOnboardingController(Get.find<UserApi>()),
Get.put(
UserOnboardingController(
Get.find<UserApi>(),
Get.find<AppleHealthUploadTool>(),
),
);
}
}
... ...
import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
... ... @@ -17,8 +20,10 @@ class UserOnboardingController extends GetxController {
final selections = <int, Set<int>>{}.obs;
final selectionVersion = 0.obs;
UserOnboardingController(this._userApi);
UserOnboardingController(this._userApi, this._appleHealthUploadTool);
final UserApi _userApi;
final AppleHealthUploadTool _appleHealthUploadTool;
final HealthKitHostApi _hostApi = HealthKitHostApi();
bool hasSelection(int pageIndex) =>
selections[pageIndex]?.isNotEmpty ?? false;
... ... @@ -204,31 +209,84 @@ class UserOnboardingController extends GetxController {
if (userStateService.isBound) {
Get.offAllNamed(AppRoutes.home);
} else {
Get.offAllNamed(AppRoutes.bindPartner);
Get.offAllNamed(AppRoutes.bindPartner, arguments: {'from': 'onboarding'});
}
}
/// 上传 Apple Health 新数据。
Future<void> _performDataUpload() async {
try {
final result =
await _appleHealthUploadTool.uploadAllNewDataFromHealthKit();
// AppLogger.i(
// 'Apple Health upload finished: '
// 'common=${result.commonUploadSuccess}(${result.commonCount}), '
// 'sleep=${result.sleepUploadSuccess}(${result.sleepCount}), '
// 'activityTarget=${result.activityTargetUploadSuccess}'
// '(${result.activityTargetCount}), '
// 'success=${result.isSuccess}',
// );
} catch (error, stackTrace) {
AppLogger.e('Apple Health upload failed', error, stackTrace);
}
}
Future<void> requestHealthAuthorization() async {
if (isAndroid) {
return;
}
try {
await Get.find<HealthKitUploadService>().requestClientAuthorization();
} catch (error) {
debugPrint('Health authorization skipped: $error');
} else {
if (GetPlatform.isIOS) {
try {
final result = await _hostApi.checkHealthAppAuthorization();
AppLogger.d("checkHealthAppAuthorization : $result");
if (result.status == 0) {
bool success = await _hostApi.requestHealthClientAuthorization();
AppLogger.d("requestHealthClientAuthorization : $success");
if (success) {
_performDataUpload();
} else {
// Get.to(NoHealthDataPage(
// onRefresh: _performDataUpload,
// ));
}
return;
}
if (result.status == 1) {
_performDataUpload();
}
} catch (e) {}
}
}
}
Future<void> requestNotificationAuthorization() async {
try {
final result = await Permission.notification.request();
if (result.isGranted) {
debugPrint('Notification authorization granted');
} else {
debugPrint('Notification authorization skipped');
final status = await Permission.notification.status;
if (status.isGranted) {
// 已授权,无需再请求
return;
}
if (status.isPermanentlyDenied) {
// 用户永久拒绝(Android),引导去设置
await openAppSettings();
return;
}
if (status.isDenied && GetPlatform.isIOS) {
// iOS 上被拒绝后无法再弹窗,引导去设置
await openAppSettings();
return;
}
// 未决定 → 弹出系统授权弹窗
final result = await Permission.notification.request();
AppLogger.d(result.isGranted
? 'Notification authorization granted'
: 'Notification authorization skipped');
} catch (error) {
debugPrint('Notification authorization skipped: $error');
AppLogger.e('Notification authorization failed: $error');
}
}
}
... ...
... ... @@ -83,6 +83,7 @@ abstract final class AppPages {
GetPage(
name: AppRoutes.login,
page: () => const LoginView(),
transition: Transition.fade,
binding: LoginBinding(),
),
GetPage(
... ... @@ -98,6 +99,7 @@ abstract final class AppPages {
GetPage(
name: AppRoutes.home,
page: () => const HomePage(),
transition: Transition.fade,
binding: HomeBinding(),
),
GetPage(
... ...
... ... @@ -22,6 +22,23 @@ class FriendApi {
);
}
Future<AppResult<FriendItem>> getFriendInfo(
int userId,
) {
return safeCall(
call: () async {
final response = await _dioClient.dio.get(
ApiPaths.friendInfo,
queryParameters: {
'friend_user_id': userId,
},
);
return FriendItem.fromJson(response.data as Map<String, dynamic>);
},
errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
);
}
Future<AppResult<FriendItem>> addFriendHandleError(
String uniqueCode, HttpErrorHandlingPolicy policy) {
return safeCall(
... ...
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import '../../constants/app_const.dart';
... ...
... ... @@ -71,4 +71,5 @@ abstract final class ApiPaths {
// friends
static const friends = '/client/doublefeel/health/v2/friends/';
static const friendInfo = '/client/doublefeel/health/v2/friend_info/';
}
... ...