feat(android): 支持沉浸式状态栏、地图定位与自定义签名
- 配置 edge-to-edge 沉浸式状态栏与刘海屏适配 - 新增百度地图权限、AK 配置及定位权限说明 - 接入 MethodChannel 实现返回键退后台热启动优化 - 配置自定义签名并更新构建逻辑 - 完善 token 刷新与鉴权失效统一处理 - 新增地图页与截图页路由 - 修复返回键拦截导致的 PopScope 失效问题
This commit is contained in:
95
lib/features/auth/data/auth_api.dart
Normal file
95
lib/features/auth/data/auth_api.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:ttstd_family_care/core/network/dio_client.dart';
|
||||
import 'package:ttstd_family_care/features/auth/domain/auth_models.dart';
|
||||
|
||||
/// 客户端(家属端)认证接口访问层。
|
||||
///
|
||||
/// 对接后端 C 端(open 模块)认证接口。`AppConstants.kBaseUrl` 仅包含 `/api/`
|
||||
/// 前缀,各端点在此显式追加 `v1/open/`(即 `/api/v1/open/**`)。open 模块账号
|
||||
/// 落地 `app_user`,与后台管理端(`sys_user`)物理分表。
|
||||
///
|
||||
/// [DioClient.get] / [DioClient.post] 已统一解析为后台返回体中的 `data` 字段;
|
||||
/// 业务失败时抛出 [ApiException],由上层(Repository)按需处理。
|
||||
class AuthApi {
|
||||
const AuthApi();
|
||||
|
||||
/// 账号密码登录。
|
||||
Future<LoginResult> loginByPassword({
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
final data = await DioClient.post(
|
||||
'v1/open/login',
|
||||
data: {
|
||||
'username': username,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
return LoginResult.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// 短信验证码登录。
|
||||
Future<LoginResult> loginBySms({
|
||||
required String phone,
|
||||
required String code,
|
||||
}) async {
|
||||
final data = await DioClient.post(
|
||||
'v1/open/auth/login/mobile',
|
||||
data: {
|
||||
'mobile': phone,
|
||||
'code': code,
|
||||
},
|
||||
);
|
||||
return LoginResult.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// 短信注册。
|
||||
Future<LoginResult> register({
|
||||
required String phone,
|
||||
required String code,
|
||||
required String password,
|
||||
}) async {
|
||||
final data = await DioClient.post(
|
||||
'v1/open/register/mobile',
|
||||
data: {
|
||||
'mobile': phone,
|
||||
'code': code,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
return LoginResult.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// 发送短信验证码。
|
||||
///
|
||||
/// 后端按场景拆分验证码发送接口(query 参数 mobile)。
|
||||
Future<void> sendSmsCode({
|
||||
required String phone,
|
||||
required SmsScene scene,
|
||||
}) async {
|
||||
final path = switch (scene) {
|
||||
SmsScene.login => 'v1/open/auth/login/sms/code',
|
||||
SmsScene.register => 'v1/open/auth/register/sms/code',
|
||||
SmsScene.resetPassword => 'v1/open/reset-password/sms/code',
|
||||
};
|
||||
await DioClient.post(
|
||||
path,
|
||||
queryParameters: {'mobile': phone},
|
||||
);
|
||||
}
|
||||
|
||||
/// 重置密码(忘记密码流程:校验验证码后设置新密码)。
|
||||
Future<void> resetPassword({
|
||||
required String phone,
|
||||
required String code,
|
||||
required String password,
|
||||
}) async {
|
||||
await DioClient.post(
|
||||
'v1/open/auth/reset-password',
|
||||
data: {
|
||||
'mobile': phone,
|
||||
'code': code,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../domain/auth_repository.dart';
|
||||
import 'auth_api.dart';
|
||||
import 'auth_repository_impl.dart';
|
||||
|
||||
/// 认证仓库 Provider。
|
||||
@@ -8,5 +9,6 @@ import 'auth_repository_impl.dart';
|
||||
/// 会话级依赖,通过 ref.keepAlive() 避免页面切换时被销毁重建。
|
||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||
ref.keepAlive();
|
||||
return const AuthRepositoryImpl();
|
||||
const api = AuthApi();
|
||||
return AuthRepositoryImpl(api);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../../../core/storage/token_storage.dart';
|
||||
import '../domain/auth_models.dart';
|
||||
import '../domain/auth_repository.dart';
|
||||
import 'auth_api.dart';
|
||||
|
||||
/// 认证仓库实现(数据层)。
|
||||
/// [AuthRepository] 的默认实现,委托给 [AuthApi]。
|
||||
///
|
||||
/// 对接后端 C 端(open 模块)认证接口,全部端点以 `/api/v1/open/auth/**` 为前缀
|
||||
/// (`AppConstants.kBaseUrl` 已包含该前缀)。open 模块账号落地 `app_user`,
|
||||
/// 与后台管理端(`sys_user`)物理分表,令牌经 `TokenManager.generateToken` 本地签发。
|
||||
/// 负责网络层([DioClient])解耦之外的业务编排:登录/注册成功后将令牌落库
|
||||
/// ([TokenStorage]),UI 仅依赖领域层抽象。
|
||||
class AuthRepositoryImpl implements AuthRepository {
|
||||
const AuthRepositoryImpl();
|
||||
const AuthRepositoryImpl(this._api);
|
||||
|
||||
final AuthApi _api;
|
||||
|
||||
@override
|
||||
Future<LoginResult> loginByPassword({
|
||||
@@ -17,14 +18,10 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
required String password,
|
||||
required String deviceId,
|
||||
}) async {
|
||||
final data = await DioClient.post(
|
||||
'/login',
|
||||
data: {
|
||||
'username': phone,
|
||||
'password': password,
|
||||
},
|
||||
final result = await _api.loginByPassword(
|
||||
username: phone,
|
||||
password: password,
|
||||
);
|
||||
final result = LoginResult.fromJson(data as Map<String, dynamic>);
|
||||
TokenStorage.saveTokens(
|
||||
token: result.token,
|
||||
refreshToken: result.refreshToken,
|
||||
@@ -38,14 +35,10 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
required String code,
|
||||
required String deviceId,
|
||||
}) async {
|
||||
final data = await DioClient.post(
|
||||
'auth/login/mobile',
|
||||
data: {
|
||||
'mobile': phone,
|
||||
'code': code,
|
||||
},
|
||||
final result = await _api.loginBySms(
|
||||
phone: phone,
|
||||
code: code,
|
||||
);
|
||||
final result = LoginResult.fromJson(data as Map<String, dynamic>);
|
||||
TokenStorage.saveTokens(
|
||||
token: result.token,
|
||||
refreshToken: result.refreshToken,
|
||||
@@ -60,15 +53,11 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
required String password,
|
||||
required String deviceId,
|
||||
}) async {
|
||||
final data = await DioClient.post(
|
||||
'/register/mobile',
|
||||
data: {
|
||||
'mobile': phone,
|
||||
'code': code,
|
||||
'password': password,
|
||||
},
|
||||
final result = await _api.register(
|
||||
phone: phone,
|
||||
code: code,
|
||||
password: password,
|
||||
);
|
||||
final result = LoginResult.fromJson(data as Map<String, dynamic>);
|
||||
TokenStorage.saveTokens(
|
||||
token: result.token,
|
||||
refreshToken: result.refreshToken,
|
||||
@@ -80,32 +69,14 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
Future<void> sendSmsCode({
|
||||
required String phone,
|
||||
required SmsScene scene,
|
||||
}) async {
|
||||
// 后端按场景拆分验证码发送接口(query 参数 mobile)
|
||||
final path = switch (scene) {
|
||||
SmsScene.login => 'auth/login/sms/code',
|
||||
SmsScene.register => 'auth/register/sms/code',
|
||||
SmsScene.resetPassword => '/reset-password/sms/code',
|
||||
};
|
||||
await DioClient.post(
|
||||
path,
|
||||
queryParameters: {'mobile': phone},
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
_api.sendSmsCode(phone: phone, scene: scene);
|
||||
|
||||
@override
|
||||
Future<void> resetPassword({
|
||||
required String phone,
|
||||
required String code,
|
||||
required String password,
|
||||
}) async {
|
||||
await DioClient.post(
|
||||
'/reset-password',
|
||||
data: {
|
||||
'mobile': phone,
|
||||
'code': code,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
_api.resetPassword(phone: phone, code: code, password: password);
|
||||
}
|
||||
|
||||
@@ -77,9 +77,15 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PopScope(
|
||||
canPop: true,
|
||||
// 拦截系统返回键:优先出栈;若无法出栈则跳回登录页。
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) context.pop();
|
||||
if (didPop) return;
|
||||
if (context.canPop()) {
|
||||
context.pop();
|
||||
} else {
|
||||
context.go('/login');
|
||||
}
|
||||
},
|
||||
child: CupertinoPageScaffold(
|
||||
backgroundColor: CupertinoColors.systemGroupedBackground,
|
||||
@@ -168,13 +174,13 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
' title=${l10n.alertTitle} message=$message');
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => CupertinoAlertDialog(
|
||||
builder: (dialogContext) => CupertinoAlertDialog(
|
||||
title: Text(l10n.alertTitle),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(l10n.confirm),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/utils/system_ui_util.dart';
|
||||
import '../../../app/theme/app_theme.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../domain/auth_models.dart';
|
||||
@@ -50,18 +51,18 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
' title=${l10n.exitConfirmTitle} content=${l10n.exitConfirmContent}');
|
||||
return showCupertinoDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => CupertinoAlertDialog(
|
||||
builder: (dialogContext) => CupertinoAlertDialog(
|
||||
title: Text(l10n.exitConfirmTitle),
|
||||
content: Text(l10n.exitConfirmContent),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(l10n.cancel),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
isDestructiveAction: true,
|
||||
child: Text(l10n.exitApp),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -88,13 +89,13 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
' title=${l10n.alertTitle} message=$message');
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => CupertinoAlertDialog(
|
||||
builder: (dialogContext) => CupertinoAlertDialog(
|
||||
title: Text(l10n.alertTitle),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(l10n.confirm),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -122,7 +123,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
await _onWillPop();
|
||||
final shouldExit = await _showExitConfirm();
|
||||
if (shouldExit == true) {
|
||||
await SystemUiUtil.moveToBack();
|
||||
}
|
||||
},
|
||||
child: CupertinoPageScaffold(
|
||||
backgroundColor: CupertinoColors.white,
|
||||
|
||||
@@ -52,9 +52,15 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PopScope(
|
||||
canPop: true,
|
||||
// 拦截系统返回键:优先出栈;若无法出栈则跳回登录页。
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) context.pop();
|
||||
if (didPop) return;
|
||||
if (context.canPop()) {
|
||||
context.pop();
|
||||
} else {
|
||||
context.go('/login');
|
||||
}
|
||||
},
|
||||
child: CupertinoPageScaffold(
|
||||
backgroundColor: CupertinoColors.systemGroupedBackground,
|
||||
@@ -144,13 +150,13 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
' title=${l10n.alertTitle} message=$message');
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => CupertinoAlertDialog(
|
||||
builder: (dialogContext) => CupertinoAlertDialog(
|
||||
title: Text(l10n.alertTitle),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(l10n.confirm),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user