|
@@ -2,6 +2,14 @@ import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
import 'package:sino_med_cloud/l10n/app_localizations.dart';
|
|
import 'package:sino_med_cloud/l10n/app_localizations.dart';
|
|
|
|
|
+import 'package:sino_med_cloud/core/constants/app_constants.dart';
|
|
|
|
|
+import '../../../core/constants/api_constants.dart';
|
|
|
|
|
+import '../../../core/network/dio_client.dart';
|
|
|
|
|
+import '../../../core/utils/logger.dart';
|
|
|
|
|
+import '../../../core/utils/crypto_utils.dart';
|
|
|
|
|
+import 'login_provider.dart';
|
|
|
|
|
+import 'package:dio/dio.dart';
|
|
|
|
|
+import 'package:path/path.dart' as path;
|
|
|
|
|
|
|
|
class LoginPage extends ConsumerStatefulWidget {
|
|
class LoginPage extends ConsumerStatefulWidget {
|
|
|
const LoginPage({super.key});
|
|
const LoginPage({super.key});
|
|
@@ -15,16 +23,19 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
|
|
late TabController _tabController;
|
|
late TabController _tabController;
|
|
|
final _passwordFormKey = GlobalKey<FormState>();
|
|
final _passwordFormKey = GlobalKey<FormState>();
|
|
|
final _smsFormKey = GlobalKey<FormState>();
|
|
final _smsFormKey = GlobalKey<FormState>();
|
|
|
|
|
+ bool _listenersSetup = false;
|
|
|
|
|
+
|
|
|
|
|
+ //用于test
|
|
|
|
|
+ final _loginSystem = "YUN_HIS_PC_WEB";
|
|
|
|
|
+ final _loginType = "MOBILE_PASSWORD";
|
|
|
|
|
|
|
|
// 密码登录表单
|
|
// 密码登录表单
|
|
|
final _phoneController = TextEditingController();
|
|
final _phoneController = TextEditingController();
|
|
|
final _passwordController = TextEditingController();
|
|
final _passwordController = TextEditingController();
|
|
|
- bool _obscurePassword = true;
|
|
|
|
|
|
|
|
|
|
// 验证码登录表单
|
|
// 验证码登录表单
|
|
|
final _phoneSmsController = TextEditingController();
|
|
final _phoneSmsController = TextEditingController();
|
|
|
final _smsCodeController = TextEditingController();
|
|
final _smsCodeController = TextEditingController();
|
|
|
- int _countdown = 0;
|
|
|
|
|
|
|
|
|
|
@override
|
|
@override
|
|
|
void initState() {
|
|
void initState() {
|
|
@@ -32,6 +43,30 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
|
|
_tabController = TabController(length: 2, vsync: this);
|
|
_tabController = TabController(length: 2, vsync: this);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ void _setupListeners() {
|
|
|
|
|
+ // 监听 Tab 切换,同步到 Provider
|
|
|
|
|
+ _tabController.addListener(() {
|
|
|
|
|
+ if (!_tabController.indexIsChanging) {
|
|
|
|
|
+ ref.read(loginTabIndexProvider.notifier).state = _tabController.index;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ // 同步初始 tab 索引
|
|
|
|
|
+ ref.read(loginTabIndexProvider.notifier).state = _tabController.index;
|
|
|
|
|
+ // 监听输入框变化,同步到 Provider
|
|
|
|
|
+ _phoneController.addListener(() {
|
|
|
|
|
+ ref.read(passwordLoginPhoneProvider.notifier).state = _phoneController.text;
|
|
|
|
|
+ });
|
|
|
|
|
+ _passwordController.addListener(() {
|
|
|
|
|
+ ref.read(passwordLoginPasswordProvider.notifier).state = _passwordController.text;
|
|
|
|
|
+ });
|
|
|
|
|
+ _phoneSmsController.addListener(() {
|
|
|
|
|
+ ref.read(smsLoginPhoneProvider.notifier).state = _phoneSmsController.text;
|
|
|
|
|
+ });
|
|
|
|
|
+ _smsCodeController.addListener(() {
|
|
|
|
|
+ ref.read(smsLoginCodeProvider.notifier).state = _smsCodeController.text;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@override
|
|
@override
|
|
|
void dispose() {
|
|
void dispose() {
|
|
|
_tabController.dispose();
|
|
_tabController.dispose();
|
|
@@ -45,7 +80,8 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
|
|
// 发送验证码
|
|
// 发送验证码
|
|
|
void _sendSmsCode() {
|
|
void _sendSmsCode() {
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
- if (_phoneSmsController.text.isEmpty) {
|
|
|
|
|
|
|
+ final phone = ref.read(smsLoginPhoneProvider);
|
|
|
|
|
+ if (phone.isEmpty) {
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
SnackBar(content: Text(l10n.phoneNumberRequiredForSms)),
|
|
SnackBar(content: Text(l10n.phoneNumberRequiredForSms)),
|
|
|
);
|
|
);
|
|
@@ -53,31 +89,56 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TODO: 调用发送验证码接口
|
|
// TODO: 调用发送验证码接口
|
|
|
- setState(() {
|
|
|
|
|
- _countdown = 60;
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // 开始倒计时
|
|
|
|
|
+ ref.read(smsCountdownProvider.notifier).state = AppConstants.smsCodeCountdown;
|
|
|
|
|
|
|
|
// 倒计时
|
|
// 倒计时
|
|
|
Future.doWhile(() async {
|
|
Future.doWhile(() async {
|
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
|
if (mounted) {
|
|
if (mounted) {
|
|
|
- setState(() {
|
|
|
|
|
- _countdown--;
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ final currentCountdown = ref.read(smsCountdownProvider);
|
|
|
|
|
+ if (currentCountdown > 0) {
|
|
|
|
|
+ ref.read(smsCountdownProvider.notifier).state = currentCountdown - 1;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- return _countdown > 0;
|
|
|
|
|
|
|
+ return ref.read(smsCountdownProvider) > 0;
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 密码登录
|
|
// 密码登录
|
|
|
- void _handlePasswordLogin() {
|
|
|
|
|
- final l10n = AppLocalizations.of(context)!;
|
|
|
|
|
|
|
+ void _handlePasswordLogin() async {
|
|
|
|
|
+ // 当前手机号及密码的格式已经验证过
|
|
|
if (_passwordFormKey.currentState!.validate()) {
|
|
if (_passwordFormKey.currentState!.validate()) {
|
|
|
- // TODO: 调用登录接口
|
|
|
|
|
- // ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
- // SnackBar(content: Text(l10n.loginNotImplemented)),
|
|
|
|
|
- // );
|
|
|
|
|
- context.go('/mainTab');
|
|
|
|
|
|
|
+ final phoneNumber = ref.watch(passwordLoginPhoneProvider);
|
|
|
|
|
+ final password = ref.watch(passwordLoginPasswordProvider);
|
|
|
|
|
+
|
|
|
|
|
+ // 对密码进行 MD5 加密
|
|
|
|
|
+ final encryptedPassword = CryptoUtils.md5(password);
|
|
|
|
|
+
|
|
|
|
|
+ final parame = {
|
|
|
|
|
+ "mobile": phoneNumber,
|
|
|
|
|
+ "login_system": _loginSystem,
|
|
|
|
|
+ // "password": '6730d7b53ea42d2b0b88ae6ba590812b',
|
|
|
|
|
+ "password": encryptedPassword,
|
|
|
|
|
+ "login_type": _loginType
|
|
|
|
|
+ };
|
|
|
|
|
+ AppLogger.d('登录请求参数parame: $parame');
|
|
|
|
|
+
|
|
|
|
|
+ Response response = await DioClient.post<Map<String, dynamic>>(
|
|
|
|
|
+ path.join(ApiConstants.baseUrl, ApiConstants.login),
|
|
|
|
|
+ data: parame,
|
|
|
|
|
+ );
|
|
|
|
|
+ if (response.statusCode == 200) {
|
|
|
|
|
+ final data = response.data;
|
|
|
|
|
+ if (data['code'] == 20000) {
|
|
|
|
|
+ AppLogger.d('登录成功: $data');
|
|
|
|
|
+ if (mounted) {
|
|
|
|
|
+ context.replace('/mainTab');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ AppLogger.d('登录失败: $data');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -89,12 +150,20 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
|
|
// ScaffoldMessenger.of(context).showSnackBar(
|
|
// ScaffoldMessenger.of(context).showSnackBar(
|
|
|
// SnackBar(content: Text(l10n.loginNotImplemented)),
|
|
// SnackBar(content: Text(l10n.loginNotImplemented)),
|
|
|
// );
|
|
// );
|
|
|
- context.go('/mainTab');
|
|
|
|
|
|
|
+ context.push('/mainTab');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
Widget build(BuildContext context) {
|
|
|
|
|
+ // 设置监听器(只设置一次)
|
|
|
|
|
+ if (!_listenersSetup) {
|
|
|
|
|
+ WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
+ _setupListeners();
|
|
|
|
|
+ _listenersSetup = true;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
return Scaffold(
|
|
return Scaffold(
|
|
|
body: GestureDetector(
|
|
body: GestureDetector(
|
|
@@ -210,32 +279,35 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
|
|
),
|
|
),
|
|
|
const SizedBox(height: 16),
|
|
const SizedBox(height: 16),
|
|
|
// 密码输入
|
|
// 密码输入
|
|
|
- TextFormField(
|
|
|
|
|
- controller: _passwordController,
|
|
|
|
|
- obscureText: _obscurePassword,
|
|
|
|
|
- decoration: InputDecoration(
|
|
|
|
|
- labelText: l10n.password,
|
|
|
|
|
- hintText: l10n.passwordHint,
|
|
|
|
|
- prefixIcon: const Icon(Icons.lock_outline),
|
|
|
|
|
- suffixIcon: IconButton(
|
|
|
|
|
- icon: Icon(
|
|
|
|
|
- _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
|
|
|
|
|
|
|
+ Consumer(
|
|
|
|
|
+ builder: (context, ref, child) {
|
|
|
|
|
+ final obscurePassword = ref.watch(passwordObscureProvider);
|
|
|
|
|
+ return TextFormField(
|
|
|
|
|
+ controller: _passwordController,
|
|
|
|
|
+ obscureText: obscurePassword,
|
|
|
|
|
+ decoration: InputDecoration(
|
|
|
|
|
+ labelText: l10n.password,
|
|
|
|
|
+ hintText: l10n.passwordHint,
|
|
|
|
|
+ prefixIcon: const Icon(Icons.lock_outline),
|
|
|
|
|
+ suffixIcon: IconButton(
|
|
|
|
|
+ icon: Icon(
|
|
|
|
|
+ obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
|
|
|
|
|
+ ),
|
|
|
|
|
+ onPressed: () {
|
|
|
|
|
+ ref.read(passwordObscureProvider.notifier).state = !obscurePassword;
|
|
|
|
|
+ },
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- onPressed: () {
|
|
|
|
|
- setState(() {
|
|
|
|
|
- _obscurePassword = !_obscurePassword;
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ validator: (value) {
|
|
|
|
|
+ if (value == null || value.isEmpty) {
|
|
|
|
|
+ return l10n.passwordRequired;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value.length < AppConstants.passwordMinLength) {
|
|
|
|
|
+ return l10n.passwordMinLength;
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
},
|
|
},
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
- validator: (value) {
|
|
|
|
|
- if (value == null || value.isEmpty) {
|
|
|
|
|
- return l10n.passwordRequired;
|
|
|
|
|
- }
|
|
|
|
|
- if (value.length < 6) {
|
|
|
|
|
- return l10n.passwordMinLength;
|
|
|
|
|
- }
|
|
|
|
|
- return null;
|
|
|
|
|
|
|
+ );
|
|
|
},
|
|
},
|
|
|
),
|
|
),
|
|
|
const SizedBox(height: 8),
|
|
const SizedBox(height: 8),
|
|
@@ -311,7 +383,7 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
|
|
if (value == null || value.isEmpty) {
|
|
if (value == null || value.isEmpty) {
|
|
|
return l10n.smsCodeRequired;
|
|
return l10n.smsCodeRequired;
|
|
|
}
|
|
}
|
|
|
- if (value.length != 6) {
|
|
|
|
|
|
|
+ if (value.length != AppConstants.smsCodeLength) {
|
|
|
return l10n.smsCodeLength;
|
|
return l10n.smsCodeLength;
|
|
|
}
|
|
}
|
|
|
return null;
|
|
return null;
|
|
@@ -319,27 +391,32 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
const SizedBox(width: 12),
|
|
const SizedBox(width: 12),
|
|
|
- SizedBox(
|
|
|
|
|
- width: 100,
|
|
|
|
|
- child: ElevatedButton(
|
|
|
|
|
- onPressed: _countdown > 0 ? null : _sendSmsCode,
|
|
|
|
|
- style: ElevatedButton.styleFrom(
|
|
|
|
|
- padding: const EdgeInsets.symmetric(vertical: 16),
|
|
|
|
|
- backgroundColor: _countdown > 0
|
|
|
|
|
- ? const Color(0xFFE5E7EB)
|
|
|
|
|
- : const Color(0xFF00BFA5),
|
|
|
|
|
- ),
|
|
|
|
|
- child: Text(
|
|
|
|
|
- _countdown > 0
|
|
|
|
|
- ? l10n.smsCodeCountdown(_countdown)
|
|
|
|
|
- : l10n.getSmsCode,
|
|
|
|
|
- style: TextStyle(
|
|
|
|
|
- color: _countdown > 0
|
|
|
|
|
- ? const Color(0xFF6B7280)
|
|
|
|
|
- : Colors.white,
|
|
|
|
|
|
|
+ Consumer(
|
|
|
|
|
+ builder: (context, ref, child) {
|
|
|
|
|
+ final countdown = ref.watch(smsCountdownProvider);
|
|
|
|
|
+ return SizedBox(
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ child: ElevatedButton(
|
|
|
|
|
+ onPressed: countdown > 0 ? null : _sendSmsCode,
|
|
|
|
|
+ style: ElevatedButton.styleFrom(
|
|
|
|
|
+ padding: const EdgeInsets.symmetric(vertical: 16),
|
|
|
|
|
+ backgroundColor: countdown > 0
|
|
|
|
|
+ ? const Color(0xFFE5E7EB)
|
|
|
|
|
+ : const Color(0xFF00BFA5),
|
|
|
|
|
+ ),
|
|
|
|
|
+ child: Text(
|
|
|
|
|
+ countdown > 0
|
|
|
|
|
+ ? l10n.smsCodeCountdown(countdown)
|
|
|
|
|
+ : l10n.getSmsCode,
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ color: countdown > 0
|
|
|
|
|
+ ? const Color(0xFF6B7280)
|
|
|
|
|
+ : Colors.white,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ );
|
|
|
|
|
+ },
|
|
|
),
|
|
),
|
|
|
],
|
|
],
|
|
|
),
|
|
),
|