网络请求打通,ws 打通
This commit is contained in:
@@ -1,45 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/ui/base/context_theme_ext.dart';
|
||||
import '../presentation/login_view_model.dart';
|
||||
import 'package:im_app/features/login/presentation/login_state.dart';
|
||||
import 'package:im_app/features/login/presentation/login_view_model.dart';
|
||||
import 'package:im_app/features/login/view/widgets/login_otp_step.dart';
|
||||
import 'package:im_app/features/login/view/widgets/login_phone_step.dart';
|
||||
|
||||
/// 登录页(Demo)
|
||||
/// 登录页 — 两步流程:手机号 → 验证码
|
||||
///
|
||||
/// 演示 go_router 登录守卫:点击「登录」后经由 [LoginViewModel.demoLogin]
|
||||
/// 触发 [GoRouter.refreshListenable],守卫重新执行并重定向到 /chat。
|
||||
/// 步骤 1 [LoginStep.phone]:[LoginPhoneStep] — 输入国家代码 + 手机号
|
||||
/// 步骤 2 [LoginStep.otp]:[LoginOtpStep] — 输入验证码完成登录
|
||||
///
|
||||
/// 正式实现时替换为完整登录流程(email/password 输入 → LoginViewModel.login)。
|
||||
class LoginPage extends ConsumerWidget {
|
||||
/// 页面本身只持有两个 TextEditingController 和三个回调方法,
|
||||
/// 具体 UI 由 widgets/ 下的子组件负责。
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// ref.watch 保持 loginViewModelProvider 存活(AutoDispose 需要至少一个监听者)
|
||||
ConsumerState<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
// demo 预填,上线前去掉
|
||||
final _phoneCtrl = TextEditingController(text: '83465308');
|
||||
final _otpCtrl = TextEditingController(text: '0000');
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneCtrl.dispose();
|
||||
_otpCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _sendOtp(LoginState state) {
|
||||
ref
|
||||
.read(loginViewModelProvider.notifier)
|
||||
.sendOtp(state.countryCode, _phoneCtrl.text.trim());
|
||||
}
|
||||
|
||||
void _verifyAndLogin() {
|
||||
ref
|
||||
.read(loginViewModelProvider.notifier)
|
||||
.verifyAndLogin(_otpCtrl.text.trim());
|
||||
}
|
||||
|
||||
void _backToPhone() {
|
||||
_otpCtrl.clear();
|
||||
ref.read(loginViewModelProvider.notifier).backToPhone();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(loginViewModelProvider);
|
||||
final s = context.styles;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('登录'), automaticallyImplyLeading: false),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('IM_Demo', style: s.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'未登录时任意路由均被重定向到此页 \n 主要是为了展示路由守卫的功能 \n 后续路由守卫专门处理各种跳转前的逻辑判断',
|
||||
style: s.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton(
|
||||
onPressed: state.isLoading
|
||||
? null
|
||||
: () => ref.read(loginViewModelProvider.notifier).demoLogin(),
|
||||
child: const Text('登录'),
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: AppBar(automaticallyImplyLeading: false, title: const Text('登录')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: state.step == LoginStep.phone
|
||||
? LoginPhoneStep(
|
||||
phoneCtrl: _phoneCtrl,
|
||||
state: state,
|
||||
onSendOtp: () => _sendOtp(state),
|
||||
)
|
||||
: LoginOtpStep(
|
||||
otpCtrl: _otpCtrl,
|
||||
state: state,
|
||||
onVerifyAndLogin: _verifyAndLogin,
|
||||
onBackToPhone: _backToPhone,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:im_app/features/login/presentation/login_state.dart';
|
||||
|
||||
/// 登录步骤 2 — 输入验证码并完成登录
|
||||
///
|
||||
/// 纯展示组件,所有交互通过回调传出,不持有任何状态。
|
||||
///
|
||||
/// 使用方式:
|
||||
/// ```dart
|
||||
/// LoginOtpStep(
|
||||
/// otpCtrl: _otpCtrl,
|
||||
/// state: state,
|
||||
/// onVerifyAndLogin: _verifyAndLogin,
|
||||
/// onBackToPhone: _backToPhone,
|
||||
/// )
|
||||
/// ```
|
||||
class LoginOtpStep extends StatelessWidget {
|
||||
const LoginOtpStep({
|
||||
super.key,
|
||||
required this.otpCtrl,
|
||||
required this.state,
|
||||
required this.onVerifyAndLogin,
|
||||
required this.onBackToPhone,
|
||||
});
|
||||
|
||||
final TextEditingController otpCtrl;
|
||||
final LoginState state;
|
||||
final VoidCallback onVerifyAndLogin;
|
||||
final VoidCallback onBackToPhone;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'输入验证码',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'验证码已发送至 ${state.countryCode} ${state.maskedContact}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
controller: otpCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 4,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '4 位验证码',
|
||||
border: OutlineInputBorder(),
|
||||
counterText: '',
|
||||
),
|
||||
autofillHints: const [AutofillHints.oneTimeCode],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (state.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
state.error!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: state.isLoading ? null : onVerifyAndLogin,
|
||||
child: state.isLoading
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('登录'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: state.isLoading ? null : onBackToPhone,
|
||||
child: const Text('返回修改手机号'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:im_app/features/login/presentation/login_state.dart';
|
||||
|
||||
/// 登录步骤 1 — 输入国家代码 + 手机号
|
||||
///
|
||||
/// 纯展示组件,所有交互通过回调传出,不持有任何状态。
|
||||
///
|
||||
/// 使用方式:
|
||||
/// ```dart
|
||||
/// LoginPhoneStep(
|
||||
/// phoneCtrl: _phoneCtrl,
|
||||
/// state: state,
|
||||
/// onSendOtp: () => _sendOtp(state),
|
||||
/// )
|
||||
/// ```
|
||||
class LoginPhoneStep extends StatelessWidget {
|
||||
const LoginPhoneStep({
|
||||
super.key,
|
||||
required this.phoneCtrl,
|
||||
required this.state,
|
||||
required this.onSendOtp,
|
||||
});
|
||||
|
||||
final TextEditingController phoneCtrl;
|
||||
final LoginState state;
|
||||
final VoidCallback onSendOtp;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'手机号登录',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
state.countryCode,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: phoneCtrl,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '手机号',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
autofillHints: const [AutofillHints.telephoneNumber],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (state.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
state.error!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: state.isLoading ? null : onSendOtp,
|
||||
child: state.isLoading
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('获取验证码'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user