| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:go_router/go_router.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 {
- const LoginPage({super.key});
- @override
- ConsumerState<LoginPage> createState() => _LoginPageState();
- }
- class _LoginPageState extends ConsumerState<LoginPage>
- with SingleTickerProviderStateMixin {
- late TabController _tabController;
- final _passwordFormKey = GlobalKey<FormState>();
- final _smsFormKey = GlobalKey<FormState>();
- bool _listenersSetup = false;
- //用于test
- final _loginSystem = "YUN_HIS_PC_WEB";
- final _loginType = "MOBILE_PASSWORD";
- // 密码登录表单
- final _phoneController = TextEditingController();
- final _passwordController = TextEditingController();
- // 验证码登录表单
- final _phoneSmsController = TextEditingController();
- final _smsCodeController = TextEditingController();
- @override
- void initState() {
- super.initState();
- _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
- void dispose() {
- _tabController.dispose();
- _phoneController.dispose();
- _passwordController.dispose();
- _phoneSmsController.dispose();
- _smsCodeController.dispose();
- super.dispose();
- }
- // 发送验证码
- void _sendSmsCode() {
- final l10n = AppLocalizations.of(context)!;
- final phone = ref.read(smsLoginPhoneProvider);
- if (phone.isEmpty) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(l10n.phoneNumberRequiredForSms)),
- );
- return;
- }
- // TODO: 调用发送验证码接口
- // 开始倒计时
- ref.read(smsCountdownProvider.notifier).state = AppConstants.smsCodeCountdown;
- // 倒计时
- Future.doWhile(() async {
- await Future.delayed(const Duration(seconds: 1));
- if (mounted) {
- final currentCountdown = ref.read(smsCountdownProvider);
- if (currentCountdown > 0) {
- ref.read(smsCountdownProvider.notifier).state = currentCountdown - 1;
- }
- }
- return ref.read(smsCountdownProvider) > 0;
- });
- }
- // 密码登录
- void _handlePasswordLogin() async {
- // 当前手机号及密码的格式已经验证过
- if (_passwordFormKey.currentState!.validate()) {
- 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');
- }
- }
- }
- }
- // 验证码登录
- void _handleSmsLogin() {
- final l10n = AppLocalizations.of(context)!;
- if (_smsFormKey.currentState!.validate()) {
- // TODO: 调用登录接口
- // ScaffoldMessenger.of(context).showSnackBar(
- // SnackBar(content: Text(l10n.loginNotImplemented)),
- // );
- context.push('/mainTab');
- }
- }
- @override
- Widget build(BuildContext context) {
- // 设置监听器(只设置一次)
- if (!_listenersSetup) {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- _setupListeners();
- _listenersSetup = true;
- });
- }
-
- final l10n = AppLocalizations.of(context)!;
- return Scaffold(
- body: GestureDetector(
- onTap: () {
- // 点击空白区域时收起键盘并移除焦点
- FocusScope.of(context).unfocus();
- },
- behavior: HitTestBehavior.opaque,
- child: SafeArea(
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(24),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- const SizedBox(height: 40),
- // Logo 或标题
- Text(
- l10n.appName,
- style: const TextStyle(
- fontSize: 32,
- fontWeight: FontWeight.bold,
- color: Color(0xFF1F2937),
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 8),
- Text(
- l10n.appSubtitle,
- style: const TextStyle(
- fontSize: 16,
- color: Color(0xFF6B7280),
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 48),
- // Tab 切换
- Container(
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(12),
- ),
- child: TabBar(
- controller: _tabController,
- indicator: BoxDecoration(
- borderRadius: BorderRadius.circular(12),
- color: const Color(0xFF00BFA5),
- ),
- indicatorSize: TabBarIndicatorSize.tab,
- dividerColor: Colors.transparent,
- labelColor: Colors.white,
- unselectedLabelColor: const Color(0xFF6B7280),
- labelStyle: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w600,
- ),
- unselectedLabelStyle: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w500,
- ),
- tabs: [
- Tab(text: l10n.passwordLogin),
- Tab(text: l10n.smsLogin),
- ],
- ),
- ),
- const SizedBox(height: 24),
- // Tab 内容
- SizedBox(
- height: 400,
- child: TabBarView(
- controller: _tabController,
- children: [
- _buildPasswordLoginForm(),
- _buildSmsLoginForm(),
- ],
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- );
- }
- // 密码登录表单
- Widget _buildPasswordLoginForm() {
- final l10n = AppLocalizations.of(context)!;
- return Form(
- key: _passwordFormKey,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- const SizedBox(height: 4),
- // 手机号输入
- TextFormField(
- controller: _phoneController,
- keyboardType: TextInputType.phone,
- decoration: InputDecoration(
- labelText: l10n.phoneNumber,
- hintText: l10n.phoneNumberHint,
- prefixIcon: const Icon(Icons.phone_outlined),
- ),
- validator: (value) {
- if (value == null || value.isEmpty) {
- return l10n.phoneNumberRequired;
- }
- if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) {
- return l10n.phoneNumberInvalid;
- }
- return null;
- },
- ),
- const SizedBox(height: 16),
- // 密码输入
- 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;
- },
- ),
- ),
- validator: (value) {
- if (value == null || value.isEmpty) {
- return l10n.passwordRequired;
- }
- if (value.length < AppConstants.passwordMinLength) {
- return l10n.passwordMinLength;
- }
- return null;
- },
- );
- },
- ),
- const SizedBox(height: 8),
- // 忘记密码
- Align(
- alignment: Alignment.centerRight,
- child: TextButton(
- onPressed: () {
- // TODO: 跳转到忘记密码页面
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(l10n.forgotPasswordNotImplemented)),
- );
- },
- child: Text(l10n.forgotPassword),
- ),
- ),
- const SizedBox(height: 24),
- // 登录按钮
- ElevatedButton(
- onPressed: _handlePasswordLogin,
- style: ElevatedButton.styleFrom(
- padding: const EdgeInsets.symmetric(vertical: 16),
- ),
- child: Text(l10n.login),
- ),
- ],
- ),
- );
- }
- // 验证码登录表单
- Widget _buildSmsLoginForm() {
- final l10n = AppLocalizations.of(context)!;
- return Form(
- key: _smsFormKey,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- const SizedBox(height: 4),
- // 手机号输入
- TextFormField(
- controller: _phoneSmsController,
- keyboardType: TextInputType.phone,
- decoration: InputDecoration(
- labelText: l10n.phoneNumber,
- hintText: l10n.phoneNumberHint,
- prefixIcon: const Icon(Icons.phone_outlined),
- ),
- validator: (value) {
- if (value == null || value.isEmpty) {
- return l10n.phoneNumberRequired;
- }
- if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) {
- return l10n.phoneNumberInvalid;
- }
- return null;
- },
- ),
- const SizedBox(height: 16),
- // 验证码输入
- Row(
- children: [
- Expanded(
- child: TextFormField(
- controller: _smsCodeController,
- keyboardType: TextInputType.number,
- decoration: InputDecoration(
- labelText: l10n.smsCode,
- hintText: l10n.smsCodeHint,
- prefixIcon: const Icon(Icons.sms_outlined),
- ),
- validator: (value) {
- if (value == null || value.isEmpty) {
- return l10n.smsCodeRequired;
- }
- if (value.length != AppConstants.smsCodeLength) {
- return l10n.smsCodeLength;
- }
- return null;
- },
- ),
- ),
- const SizedBox(width: 12),
- 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,
- ),
- ),
- ),
- );
- },
- ),
- ],
- ),
- const SizedBox(height: 24),
- // 登录按钮
- ElevatedButton(
- onPressed: _handleSmsLogin,
- style: ElevatedButton.styleFrom(
- padding: const EdgeInsets.symmetric(vertical: 16),
- ),
- child: Text(l10n.login),
- ),
- ],
- ),
- );
- }
- }
|