Commit ee39088f by tanghuan

新的登录UI,以及其他一些调整

1 parent 51118024
Showing 64 changed files with 836 additions and 476 deletions
import 'dart:async';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/data/repositories/phone_auth_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LoginPhoneState extends Equatable {
final bool agreed;
final bool showAgreed;
final String phone;
final String password;
final bool showSnackBar;
final String snackBarMsg;
final bool allowSend;
final int seconds;
const LoginPhoneState(
{this.agreed = false,
this.showAgreed = false,
this.phone = '',
this.password = '',
this.allowSend = true,
this.seconds = 0});
const LoginPhoneState({
this.agreed = false,
this.showAgreed = false,
this.showSnackBar = false,
this.snackBarMsg = '',
this.allowSend = true,
this.seconds = 0,
});
LoginPhoneState copyWith({
bool? agreed,
bool? showAgreed,
String? phone,
String? password,
bool? showSnackBar,
String? snackBarMsg,
bool? allowSend,
int? seconds,
}) {
return LoginPhoneState(
agreed: agreed ?? this.agreed,
showAgreed: showAgreed ?? this.showAgreed,
phone: phone ?? this.phone,
password: password ?? this.password,
showSnackBar: showSnackBar ?? this.showSnackBar,
snackBarMsg: snackBarMsg ?? this.snackBarMsg,
allowSend: allowSend ?? this.allowSend,
seconds: seconds ?? this.seconds,
);
}
@override
List<Object?> get props => [phone, password, agreed, showAgreed, allowSend, seconds];
List<Object?> get props => [
agreed,
showAgreed,
showSnackBar,
snackBarMsg,
allowSend,
seconds,
];
}
class LoginPhoneCubit extends Cubit<LoginPhoneState> {
......@@ -49,6 +61,8 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
Timer? _timer;
int countdown = 60; // 倒计时秒数
late final PhoneAuthRepository _phoneAuthRepository;
TextEditingController get phoneController => _phoneController;
TextEditingController get codeController => _codeController;
......@@ -57,8 +71,10 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
_phoneController = TextEditingController();
_codeController = TextEditingController();
_phoneController.text = state.phone;
_codeController.text = state.password;
_phoneController.text = '';
_codeController.text = '';
_phoneAuthRepository = getIt.get<PhoneAuthRepository>();
}
/// 开始倒计时
......@@ -78,21 +94,107 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
}
/// 发送验证码
void sendVerificationCode() {
Future<void> sendVerificationCode() async {
if (state.allowSend) {
// 实际发送验证码的逻辑
startCountdown(); // 开始倒计时
// 验证手机号码
String phone = _phoneController.text;
if (!RegExp(r'^1[3-9][0-9]{9}$').hasMatch(phone)) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: '请输入正确的手机号码'));
emit(state.copyWith(showSnackBar: false));
return;
}
// 发送验证码
var result = await _phoneAuthRepository.verifyCode(phone);
if (result['code'] != 0) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error']));
emit(state.copyWith(showSnackBar: false));
return;
}
emit(state.copyWith(allowSend: false, seconds: 60));
// 开始倒计时
startCountdown();
}
}
Future<void> phoneAuth() async {
String phone = _phoneController.text;
String verifyCode = _codeController.text;
if (!RegExp(r'^1[3-9][0-9]{9}$').hasMatch(phone)) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: '请输入正确的手机号码'));
emit(state.copyWith(showSnackBar: false));
return;
}
if (!RegExp(r'^\d{4}$').hasMatch(verifyCode)) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: '请输入正确的验证码'));
emit(state.copyWith(showSnackBar: false));
return;
}
if (!state.agreed) {
emit(state.copyWith(showAgreed: true));
return;
}
var result = await _phoneAuthRepository.login(phone, verifyCode);
if (result['code'] != 1) {
emit(state.copyWith(showSnackBar: true, snackBarMsg: result['error']));
return;
}
var data = result['data'];
var role = data['roles'][0];
final sessionCode = data['sessionCode'];
final userCode = data['userCode'];
final classCode = role['classCode'];
final userType = role['userType'];
final stuId = role['stuId'];
var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.setString('auth_sessionCode', sessionCode);
sharedPreferences.setString('auth_userCode', userCode);
sharedPreferences.setString('auth_classCode', classCode);
sharedPreferences.setInt('auth_userType', userType);
sharedPreferences.setString('auth_stuId', stuId ?? '');
router.go(
'/web',
extra: {
'sessionCode': sessionCode,
'userCode': userCode,
'classCode': classCode,
'userType': userType,
'stuId': stuId,
},
);
}
void toggleAgreed(bool value) {
emit(state.copyWith(agreed: value));
}
void confirmAgreed() {
emit(state.copyWith(agreed: true, showAgreed: false));
phoneAuth();
}
void cancelAgreed() {
emit(state.copyWith(showAgreed: false));
}
void goLoginMain() {
router.go('/loginMain');
}
void goLoginQr() {
router.go('/loginQr');
}
@override
Future<void> close() async {
try {
......
import 'dart:convert';
import 'dart:typed_data';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart';
......@@ -66,11 +67,11 @@ class LoginQrCubit extends Cubit<LoginQrState> {
// 当前时间戳
var timestamp = DateTime.now().millisecondsSinceEpoch;
var signature = _sig('wx8c32ea248f0c7765', '$timestamp', sdkTicket, '$timestamp');
var signature = _sig(Constant.wxAppId, '$timestamp', sdkTicket, '$timestamp');
var authResult = await _fluwx.authBy(
which: QRCode(
appId: 'wx8c32ea248f0c7765',
appId: Constant.wxAppId,
scope: 'snsapi_userinfo',
nonceStr: '$timestamp',
timestamp: '$timestamp',
......@@ -84,14 +85,21 @@ class LoginQrCubit extends Cubit<LoginQrState> {
if (response is WeChatAuthGotQRCodeResponse) {
int? errCode = response.errCode;
if (errCode != null && errCode == 0) {
emit(state.copyWith(status: 1, image: response.qrCode, tip: '打开微信,扫描二维码登录'));
emit(state.copyWith(
status: 1,
image: response.qrCode,
tip: '打开微信,扫描二维码登录',
));
} else {
emit(state.copyWith(tip: '错误 $errCode'));
}
} else if (response is WeChatQRCodeScannedResponse) {
int? errCode = response.errCode;
if (errCode != null && errCode == 0) {
emit(state.copyWith(status: 2, tip: '在微信中轻触允许即可登录'));
emit(state.copyWith(
status: 2,
tip: '在微信中轻触允许即可登录',
));
} else {
emit(state.copyWith(tip: '错误 $errCode'));
}
......@@ -100,7 +108,10 @@ class LoginQrCubit extends Cubit<LoginQrState> {
if (errCode != null && errCode == 0) {
_doLogin(response.authCode!);
} else {
emit(state.copyWith(status: 3, tip: '你已取消此次登录'));
emit(state.copyWith(
status: 3,
tip: '你已取消此次登录',
));
}
}
}
......
......@@ -55,15 +55,24 @@ class Constant {
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
static const String bxeBaseUrl = EnvConfig.env == 'dev' ? 'https://dev.banxiaoer.net' : 'https://bxr.banxiaoer.com';
static const String iotAppBaseUrl = EnvConfig.env == 'dev' ? 'https://iotapp-dev.banxiaoer.com/iotapp' : 'https://iotapp.banxiaoer.com/iotapp';
static const String iotAppBaseUrl =
EnvConfig.env == 'dev' ? 'https://iotapp-dev.banxiaoer.com/iotapp' : 'https://iotapp.banxiaoer.com/iotapp';
/// 微信登录相关
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
static const String wxAppId = 'wx8c32ea248f0c7765';
static const String universalLink = 'https://dev.banxiaoer.net/path/to/wechat/';
/// IM 相关
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
/// IM SDK
static const int imSdkAppId = EnvConfig.env == 'dev' ? 1400310691 : 0;
static const String imClientSecure =
EnvConfig.env == 'dev' ? 'kM4yqbehB3io9UiLvH6eHvM7xAhfYxoyyaO1tLoHgKltcaI7MZXkUbpFaWdeQIqe' : '';
static const int imSdkAppId = EnvConfig.env == 'dev' ? 1400310691 : 1600117207;
static const String imClientSecure = EnvConfig.env == 'dev'
? 'kM4yqbehB3io9UiLvH6eHvM7xAhfYxoyyaO1tLoHgKltcaI7MZXkUbpFaWdeQIqe'
: 'GkMkhAnrCThYrZxApCBdFidcAC8USwVnhoqMGzqmSvmcegRCvETtDR2Te9btarnG';
/// 测试阶段使用
static const bool needIM = false;
......
......@@ -31,6 +31,7 @@ import 'package:appframe/data/repositories/message/vibrate_short_handler.dart';
import 'package:appframe/data/repositories/message/video_info_handler.dart';
import 'package:appframe/data/repositories/message/wifi_info_handler.dart';
import 'package:appframe/data/repositories/message/window_info_handler.dart';
import 'package:appframe/data/repositories/phone_auth_repository.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/dispatcher.dart';
......@@ -51,8 +52,8 @@ Future<void> setupLocator() async {
Fluwx fluwx = Fluwx();
if (Platform.isAndroid || Platform.isIOS) {
await fluwx.registerApi(
appId: "wx8c32ea248f0c7765",
universalLink: "https://dev.banxiaoer.net/path/to/wechat/",
appId: Constant.wxAppId,
universalLink: Constant.universalLink,
);
}
return fluwx;
......@@ -177,7 +178,7 @@ Future<void> setupLocator() async {
getIt.registerLazySingleton<MessageHandler>(() => TitleBarHandler(), instanceName: 'setTitlebar');
/// 新路由打开链接
getIt.registerLazySingleton<MessageHandler>(() => OpenLinkHandler(), instanceName: 'openlink');
getIt.registerLazySingleton<MessageHandler>(() => OpenLinkHandler(), instanceName: 'openLink');
/// 登录
getIt.registerLazySingleton<MessageHandler>(() => GoLoginHandler(), instanceName: 'goLogin');
......@@ -208,4 +209,5 @@ Future<void> setupLocator() async {
/// repository
getIt.registerLazySingleton<WechatAuthRepository>(() => WechatAuthRepository());
getIt.registerLazySingleton<PhoneAuthRepository>(() => PhoneAuthRepository());
}
import 'package:appframe/config/locator.dart';
import 'package:appframe/services/api_service.dart';
import 'package:dio/dio.dart';
class PhoneAuthRepository {
late final ApiService _appService;
PhoneAuthRepository() {
_appService = getIt<ApiService>(instanceName: 'appApiService');
}
///
/// {
/// "code": 0,
/// "error": "绑定成功"
/// }
///
Future<dynamic> bind(String userid, String phone, String verifyCode) async {
Response resp = await _appService.post(
'/api/v1/comm/phone/bind',
{
"userid": userid,
"phone": phone,
"verifyCode": verifyCode,
},
);
return resp.data;
}
///
/// {
/// "code": 0,
/// "error": "获取成功",
/// }
///
Future<dynamic> verifyCode(String phone) async {
Response resp = await _appService.get(
'/api/v1/comm/phone/verifycode',
queryParameters: {
"phone": phone,
"type": 1,
},
);
return resp.data;
}
///
/// {
/// "code": 0,
/// "error": "登录成功",
/// "userid": "user123"
/// }
///
Future<dynamic> login(String phone, String verifyCode) async {
Response resp = await _appService.post(
'/api/v1/comm/phone/login',
{
"phone": phone,
"verifyCode": verifyCode,
},
);
return resp.data;
}
}
import 'package:appframe/bloc/login_main_cubit.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/ui/widgets/login/login_page_agreed_widget.dart';
import 'package:appframe/ui/widgets/login/login_page_header_widget.dart';
import 'package:appframe/ui/widgets/login/login_page_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluwx/fluwx.dart';
......@@ -14,198 +12,167 @@ class LoginMainPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LoginMainCubit(LoginMainState()),
child: BlocConsumer<LoginMainCubit, LoginMainState>(builder: (context, state) {
var loginMainCubit = context.read<LoginMainCubit>();
return Stack(
children: [
Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.white,
body: SafeArea(
top: false,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LoginPageImageWidget(),
Transform.translate(
offset: Offset(0, -40), // 向上移动40像素
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
color: Colors.white, // 设置背景色
child: BlocConsumer<LoginMainCubit, LoginMainState>(
builder: (context, state) {
var loginMainCubit = context.read<LoginMainCubit>();
return Stack(
children: [
Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
top: false,
child: SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 100),
Center(
child: Image.asset(
'assets/images/login_v2/banner_1.png',
),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 30),
LoginPageHeaderWidget(),
SizedBox(height: 25),
_buildLoginButtons(context, loginMainCubit, state.agreed),
SizedBox(height: 20),
_buildAgreement(context, loginMainCubit, state.agreed),
],
),
SizedBox(height: 30),
Center(
child: Image.asset(
'assets/images/login_v2/main.png',
),
),
),
],
SizedBox(height: 40),
Padding(
padding: EdgeInsets.symmetric(horizontal: 42.5),
child: SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () async {
if (await getIt.get<Fluwx>().isWeChatInstalled) {
loginMainCubit.wechatAuth();
} else {
_showWechatNotInstallDialog(context);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF26C445),
foregroundColor: Colors.white,
textStyle: TextStyle(fontSize: 19),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(27),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/login_v2/wechat_white_icon.png'),
SizedBox(width: 4.5),
Text('微信登录'),
],
),
),
),
),
SizedBox(height: 15),
_buildAgreement(context, loginMainCubit, state.agreed),
SizedBox(height: 82.5),
Text(
'其他方式登录',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
strutStyle: StrutStyle(height: 16 / 14),
),
SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
context.read<LoginMainCubit>().goLoginQr();
},
child: Image.asset(
'assets/images/login_v2/qr_green_icon.png',
),
),
SizedBox(width: 25),
InkWell(
onTap: () {
context.read<LoginMainCubit>().goLoginPhone();
},
child: Image.asset(
'assets/images/login_v2/phone_blue_icon.png',
),
),
],
),
],
),
),
),
),
),
state.loading
? Container(
color: Colors.black54,
width: MediaQuery.of(context).size.width,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Color(0xFF7691FA),
),
],
state.loading
? Container(
color: Colors.black54,
width: MediaQuery.of(context).size.width,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Color(0xFF7691FA),
),
],
),
),
),
)
: SizedBox(),
],
);
}, listener: (context, state) {
if (state.showAgreed) {
_showAgreementDialog(context, context.read<LoginMainCubit>());
}
}),
)
: SizedBox(),
],
);
},
listener: (context, state) {
if (state.showAgreed) {
_showAgreementDialog(context, context.read<LoginMainCubit>());
}
},
),
);
}
Widget _buildLoginButtons(BuildContext context, LoginMainCubit loginMainCubit, bool agreed) {
return Column(
Widget _buildAgreement(BuildContext context, LoginMainCubit loginMainCubit, bool agreed) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: double.infinity,
height: 47,
child: ElevatedButton(
onPressed: () async {
if (await getIt.get<Fluwx>().isWeChatInstalled) {
loginMainCubit.wechatAuth();
} else {
_noWechatLogin(context);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF00CB60),
foregroundColor: Colors.white,
textStyle: TextStyle(fontSize: 19),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.5),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/login/wechat_white_icon.png'),
SizedBox(width: 6),
Text('微信登录'),
],
),
),
),
SizedBox(height: 15),
SizedBox(
width: double.infinity,
height: 47,
child: ElevatedButton(
onPressed: () {
loginMainCubit.goLoginQr();
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF00CB60),
foregroundColor: Colors.white,
textStyle: TextStyle(fontSize: 19),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.5),
Theme(
data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData(
shape: CircleBorder(
side: BorderSide(width: 0.5, color: Color(0xFF999999)),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/login/wechat_white_icon.png'),
SizedBox(width: 6),
Text('扫码登录'),
],
),
),
),
SizedBox(height: 15),
SizedBox(
width: double.infinity,
height: 47,
child: ElevatedButton(
onPressed: () {
loginMainCubit.goLoginPhone();
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF7691FA),
foregroundColor: Colors.white,
textStyle: TextStyle(fontSize: 19),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.5),
fillColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return Color(0xFF7691FA); // 选中时的颜色
}
return Colors.white; // 未选中时的颜色
},
),
checkColor: WidgetStateProperty.all<Color>(Colors.white),
side: BorderSide(width: 0.5, color: Color(0xFF999999)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/login/phone_white_icon.png'),
// SizedBox(width: 8),
Text('手机号登录'),
],
),
cardColor: Colors.white,
unselectedWidgetColor: Color(0xFF999999),
),
),
],
);
}
Widget _buildAgreement(BuildContext context, LoginMainCubit loginMainCubit, bool agreed) {
return Padding(
padding: const EdgeInsets.only(bottom: 32.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Theme(
data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData(
shape: CircleBorder(
side: BorderSide(width: 0.5, color: Color(0xFF999999)),
),
fillColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return Color(0xFF7691FA); // 选中时的颜色
}
return Colors.white; // 未选中时的颜色
},
),
checkColor: WidgetStateProperty.all<Color>(Colors.white),
side: BorderSide(width: 0.5, color: Color(0xFF999999)),
),
cardColor: Colors.white,
unselectedWidgetColor: Color(0xFF999999),
),
child: SizedBox(
height: 19, // 设置高度为19
child: Checkbox(
value: agreed,
onChanged: (bool? value) {
loginMainCubit.toggleAgreed(value!);
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, // 缩小点击区域,减小间隔
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
LoginPageAgreedWidget(),
],
),
),
LoginPageAgreedWidget(),
],
);
}
......@@ -308,20 +275,57 @@ class LoginMainPage extends StatelessWidget {
}
}
void _noWechatLogin(BuildContext ctx) {
Future<void> _showWechatNotInstallDialog(BuildContext context) async {
showDialog(
context: ctx,
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('温馨提示'),
content: Text('设备上未安装微信App,请选择其它登录方式'),
actions: <Widget>[
TextButton(
child: Text('关闭'),
onPressed: () {
Navigator.of(context).pop();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
title: Text(
'提示',
style: TextStyle(
fontSize: 17,
color: Color(0xFF000000),
// fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
content: Text.rich(
TextSpan(
text: '此设备未安装微信App,请选择其他方式登录。',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
),
actions: [
Table(
children: [
TableRow(
children: [
TableCell(
child: TextButton(
onPressed: () {
Navigator.of(context).pop('OK');
},
style: TextButton.styleFrom(
foregroundColor: Color(0xFF7691FA),
textStyle: TextStyle(fontSize: 17),
minimumSize: Size.fromHeight(40),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text('确定'),
),
),
],
),
],
),
],
);
......
import 'package:appframe/bloc/login_phone_cubit.dart';
import 'package:appframe/ui/widgets/login/login_page_agreed_widget.dart';
import 'package:appframe/ui/widgets/login/login_page_header_widget.dart';
import 'package:appframe/ui/widgets/login/login_page_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
......@@ -14,51 +12,77 @@ class LoginPhonePage extends StatelessWidget {
return BlocProvider(
create: (context) => LoginPhoneCubit(LoginPhoneState()),
child: BlocConsumer<LoginPhoneCubit, LoginPhoneState>(
builder: (context, state) {
var loginPhoneCubit = context.read<LoginPhoneCubit>();
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.white,
body: SafeArea(
top: false,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LoginPageImageWidget(),
Transform.translate(
offset: Offset(0, -40), // 向上移动40像素
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
color: Colors.white, // 设置背景色
builder: (ctx, state) {
var loginPhoneCubit = ctx.read<LoginPhoneCubit>();
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
top: false,
child: SingleChildScrollView(
child: Column(
children: [
Image.asset(
'assets/images/login_v2/banner_2.png',
width: MediaQuery.of(context).size.width,
fit: BoxFit.fitWidth,
),
SizedBox(height: 15.5),
Padding(
padding: EdgeInsets.symmetric(horizontal: 30),
child: Column(children: [
_buildInputFields(loginPhoneCubit, state),
SizedBox(height: 15),
_buildLoginButton(ctx),
]),
),
SizedBox(height: 15),
_buildAgreement(ctx, loginPhoneCubit, state.agreed),
SizedBox(height: 140),
Text(
'其他方式登录',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
strutStyle: StrutStyle(height: 16 / 14),
),
SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
loginPhoneCubit.goLoginMain();
},
child: Image.asset(
'assets/images/login_v2/wechat_green_icon.png',
),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 30),
LoginPageHeaderWidget(),
SizedBox(height: 25),
_buildInputFields(loginPhoneCubit, state),
SizedBox(height: 20),
_buildLoginButton(),
SizedBox(height: 30),
_buildAgreement(context, loginPhoneCubit, state.agreed),
SizedBox(height: 24.5),
_buildWechatLogin(loginPhoneCubit),
],
),
SizedBox(width: 25),
InkWell(
onTap: () {
loginPhoneCubit.goLoginQr();
},
child: Image.asset(
'assets/images/login_v2/qr_green_icon.png',
),
),
),
],
),
],
),
],
),
),
);
},
listener: (context, state) {}),
),
);
},
listener: (context, state) {
if (state.showAgreed) {
_showAgreementDialog(context, context.read<LoginPhoneCubit>());
} else if (state.showSnackBar) {
_showTip(context, state.snackBarMsg);
}
},
),
);
}
......@@ -67,10 +91,10 @@ class LoginPhonePage extends StatelessWidget {
children: [
// 手机号输入框
Container(
height: 60.5,
height: 60,
decoration: BoxDecoration(
color: Color(0xFFF7F9FF),
borderRadius: BorderRadius.vertical(top: Radius.circular(15)),
borderRadius: BorderRadius.circular(10),
),
child: TextField(
controller: loginPhoneCubit.phoneController,
......@@ -87,14 +111,16 @@ class LoginPhonePage extends StatelessWidget {
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.fromLTRB(0, 26.5, 0, 15),
contentPadding: EdgeInsets.fromLTRB(0, 22, 0, 19),
// 左右内边距,垂直居中
prefixIcon: Container(
width: 25.5,
height: 25.5,
// width: 25.5,
// height: 25.5,
margin: EdgeInsets.only(left: 15), // 控制左边距
child: Image.asset(
'assets/images/login/phone_blue_icon.png',
'assets/images/login_v2/phone_small.png',
width: 25.5,
height: 25.5,
fit: BoxFit.contain,
),
),
......@@ -105,108 +131,97 @@ class LoginPhonePage extends StatelessWidget {
),
),
),
// 增加一条线段
Padding(
padding: EdgeInsets.symmetric(horizontal: 15),
child: Divider(
color: Color(0xFFE1E7FF),
height: 0.5,
thickness: 0.5,
),
),
// 手机号输入框和发送按钮之间的间隔
SizedBox(height: 15),
// 验证码输入框和发送按钮
Row(
children: [
Expanded(
child: Container(
height: 60.5,
decoration: BoxDecoration(
color: Color(0xFFF7F9FF),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(15)),
Container(
height: 60,
decoration: BoxDecoration(
color: Color(0xFFF7F9FF),
borderRadius: BorderRadius.circular(10),
),
child: Stack(
children: [
TextField(
controller: loginPhoneCubit.codeController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4), // 使用这个来限制长度
],
decoration: InputDecoration(
hintText: '请输入验证码',
hintStyle: TextStyle(
fontSize: 18,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.fromLTRB(0, 22, 0, 19),
// 左右内边距,垂直居中
prefixIcon: Container(
margin: EdgeInsets.only(left: 15), // 控制左边距
child: Image.asset(
'assets/images/login_v2/secure_small.png',
width: 25.5,
height: 25.5,
fit: BoxFit.contain,
),
),
),
child: Stack(
children: [
TextField(
controller: loginPhoneCubit.codeController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(6), // 使用这个来限制长度
],
decoration: InputDecoration(
hintText: '请输入验证码',
hintStyle: TextStyle(
fontSize: 18,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.fromLTRB(0, 26.5, 0, 15),
// 左右内边距,垂直居中
prefixIcon: Container(
width: 25.5,
height: 25.5,
margin: EdgeInsets.only(left: 15), // 控制左边距
child: Image.asset(
'assets/images/login/security_blue_icon.png',
fit: BoxFit.contain,
),
),
),
style: TextStyle(
fontSize: 18,
color: Color(0xFF000000),
style: TextStyle(
fontSize: 18,
color: Color(0xFF000000),
),
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: Container(
width: 100,
decoration: BoxDecoration(
color: Colors.transparent,
),
child: TextButton(
onPressed: () {
if (state.allowSend) {
loginPhoneCubit.sendVerificationCode();
}
},
style: TextButton.styleFrom(
backgroundColor: Colors.transparent,
// foregroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.zero,
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: Container(
width: 100,
decoration: BoxDecoration(
color: Colors.transparent,
),
child: TextButton(
onPressed: () {
if (state.allowSend) {
loginPhoneCubit.sendVerificationCode();
}
},
style: TextButton.styleFrom(
backgroundColor: Colors.transparent,
// foregroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.zero,
),
child: Text(
state.allowSend ? '发送验证码' : '${state.seconds}s后可重发',
style: TextStyle(
fontSize: 16,
color: Color(0xFF7691FA),
fontWeight: FontWeight.normal,
),
),
),
child: Text(
state.allowSend ? '发送验证码' : '${state.seconds}s后可重发',
style: TextStyle(
fontSize: 16,
color: Color(0xFF7691FA),
fontWeight: FontWeight.normal,
),
),
],
),
),
),
),
],
],
),
),
],
);
}
Widget _buildLoginButton() {
Widget _buildLoginButton(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 47,
child: ElevatedButton(
onPressed: () {},
onPressed: () {
context.read<LoginPhoneCubit>().phoneAuth();
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF7691FA),
foregroundColor: Colors.white,
......@@ -215,66 +230,45 @@ class LoginPhonePage extends StatelessWidget {
borderRadius: BorderRadius.circular(23.5),
),
),
child: Text('手机号登录'),
),
);
}
Widget _buildWechatLogin(LoginPhoneCubit loginPhoneCubit) {
return SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
loginPhoneCubit.goLoginMain();
},
child: Column(
children: [
Image.asset('assets/images/login/wechat.png'),
SizedBox(height: 4),
Text(
'返回微信登录',
style: TextStyle(
fontSize: 14,
color: Color(0xFF000000),
),
),
],
),
child: Text(
'手机号登录',
style: TextStyle(
fontSize: 19,
fontWeight: FontWeight.w400,
color: Color(0xFFFFFFFF),
),
],
strutStyle: StrutStyle(height: 22 / 19),
),
),
);
}
Widget _buildAgreement(BuildContext context, LoginPhoneCubit loginPhoneCubit, bool agreed) {
return Padding(
padding: const EdgeInsets.only(bottom: 32.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Theme(
data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData(
shape: CircleBorder(
side: BorderSide(width: 0.5, color: Color(0xFF999999)),
),
fillColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return Color(0xFF7691FA); // 选中时的颜色
}
return Colors.white; // 未选中时的颜色
},
),
checkColor: WidgetStateProperty.all<Color>(Colors.white),
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Theme(
data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData(
shape: CircleBorder(
side: BorderSide(width: 0.5, color: Color(0xFF999999)),
),
cardColor: Colors.white,
unselectedWidgetColor: Color(0xFF999999),
fillColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return Color(0xFF7691FA); // 选中时的颜色
}
return Colors.white; // 未选中时的颜色
},
),
checkColor: WidgetStateProperty.all<Color>(Colors.white),
side: BorderSide(width: 0.5, color: Color(0xFF999999)),
),
cardColor: Colors.white,
unselectedWidgetColor: Color(0xFF999999),
),
child: SizedBox(
height: 19,
child: Checkbox(
value: agreed,
onChanged: (bool? value) {
......@@ -283,9 +277,138 @@ class LoginPhonePage extends StatelessWidget {
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, // 缩小点击区域,减小间隔
),
),
LoginPageAgreedWidget(),
],
),
LoginPageAgreedWidget(),
],
);
}
Future<void> _showAgreementDialog(BuildContext context, LoginPhoneCubit loginPhoneCubit) async {
final result = await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
title: Text(
'个人信息保护指引',
style: TextStyle(
fontSize: 17,
color: Color(0xFF000000),
// fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
content: Text.rich(
TextSpan(
children: [
TextSpan(
text: '感谢使用班小二APP,为保护您的个人权益,请仔细阅读并充分理解',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
TextSpan(
text: '《班小二数据据安全和隐私政策》',
style: TextStyle(color: Color(0xFF7691FA), fontSize: 14),
),
TextSpan(
text: '与',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
TextSpan(
text: '《用户协议》',
style: TextStyle(color: Color(0xFF7691FA), fontSize: 14),
),
TextSpan(
text: '。如您同意上述文件的全部内容,请点击“同意”以继续。',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
],
),
),
actions: [
Table(
children: [
TableRow(
children: [
TableCell(
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
style: TextButton.styleFrom(
foregroundColor: Color(0xFF666666),
textStyle: TextStyle(fontSize: 17),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text('放弃登录'),
),
),
TableCell(
child: TextButton(
onPressed: () {
Navigator.of(context).pop('OK');
},
style: TextButton.styleFrom(
foregroundColor: Color(0xFF7691FA),
textStyle: TextStyle(fontSize: 17),
minimumSize: Size.fromHeight(40),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text('同意'),
),
),
],
),
],
),
],
);
},
);
if (result != null) {
loginPhoneCubit.confirmAgreed();
} else {
loginPhoneCubit.cancelAgreed();
}
}
void _showTip(BuildContext context, String tip) {
OverlayEntry overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: 200,
left: 20,
right: 20,
child: Material(
color: Colors.transparent,
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: Text(
tip,
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
),
);
Overlay.of(context).insert(overlayEntry);
Future.delayed(Duration(seconds: 2), () {
overlayEntry.remove();
});
}
}
import 'package:appframe/bloc/login_qr_cubit.dart';
import 'package:appframe/ui/widgets/login/login_page_header_widget.dart';
import 'package:appframe/ui/widgets/login/login_page_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
......@@ -15,7 +13,6 @@ class LoginQrPage extends StatelessWidget {
builder: (context, state) {
var loginQrCubit = context.read<LoginQrCubit>();
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.white,
body: SafeArea(
top: false,
......@@ -23,31 +20,40 @@ class LoginQrPage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LoginPageImageWidget(),
Transform.translate(
offset: Offset(0, -40), // 向上移动40像素
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
color: Colors.white, // 设置背景色
),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 30),
LoginPageHeaderWidget(),
SizedBox(height: 25),
Center(
child: _buildQrCode(loginQrCubit, state),
),
SizedBox(height: 30),
_buildWechatLogin(loginQrCubit),
],
Image.asset(
'assets/images/login_v2/banner_2.png',
width: MediaQuery.of(context).size.width,
fit: BoxFit.fitWidth,
),
SizedBox(
height: 380,
child: _buildQrCode(loginQrCubit, state),
),
SizedBox(height: 20),
Center(
child: Text(
'其他方式登录',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
strutStyle: StrutStyle(height: 16 / 14),
),
),
SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
loginQrCubit.goLoginMain();
},
child: Image.asset(
'assets/images/login_v2/wechat_green_icon.png',
),
),
],
),
],
),
),
......@@ -65,33 +71,63 @@ class LoginQrPage extends StatelessWidget {
children: [
Column(
children: [
SizedBox(height: 40),
CircularProgressIndicator(),
SizedBox(height: 12),
SizedBox(height: 112),
Image.asset(
'assets/images/login_v2/loading.gif',
width: 72,
height: 72,
),
SizedBox(height: 24),
Text(
'正在生成二维码...',
style: TextStyle(
fontSize: 14,
color: Color(0xFF333333),
),
strutStyle: StrutStyle(height: 16 / 14),
),
SizedBox(height: 8),
Text(
state.tip,
style: TextStyle(
fontSize: 14,
color: Color(0xFF333333),
),
strutStyle: StrutStyle(height: 16 / 14),
),
SizedBox(height: 40),
],
),
SizedBox(height: 12),
Text(
state.tip,
),
],
);
} else if (state.status == 1) {
// 等待扫码
return Column(
children: [
Image.memory(
state.image!,
width: 200,
height: 200,
Container(
width: 230,
height: 230,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border.all(
color: Color(0x29265ECF),
width: 0.5,
),
borderRadius: BorderRadius.circular(20),
),
child: Image.memory(
state.image!,
width: 190,
height: 190,
),
),
SizedBox(height: 12),
Text(
state.tip,
style: TextStyle(
fontSize: 14,
color: Color(0xFF333333),
),
strutStyle: StrutStyle(height: 16 / 14),
),
],
);
......@@ -99,13 +135,31 @@ class LoginQrPage extends StatelessWidget {
// 已扫码,等待确认
return Column(
children: [
// Image.asset("assets/qr_suc.png"),
SizedBox(height: 15.5),
Image.asset(
"assets/images/login_v2/allowed.png",
width: 217,
height: 190,
),
SizedBox(height: 15.5),
Text(
"已扫码",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
fontFamily: '黑体',
color: Color(0xFF333333),
),
strutStyle: StrutStyle(height: 21 / 16),
),
SizedBox(height: 12),
SizedBox(height: 8),
Text(
state.tip,
style: TextStyle(
fontSize: 14,
color: Color(0xFF333333),
),
strutStyle: StrutStyle(height: 16 / 14),
),
],
);
......@@ -113,46 +167,35 @@ class LoginQrPage extends StatelessWidget {
// 拒绝
return Column(
children: [
// Image.asset("assets/qr_fail.png"),
SizedBox(height: 15.5),
Image.asset(
"assets/images/login_v2/refused.png",
width: 217,
height: 190,
),
SizedBox(height: 15.5),
Text(
"拒绝登录",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
fontFamily: '黑体',
color: Color(0xFF333333),
),
strutStyle: StrutStyle(height: 21 / 16),
),
SizedBox(height: 12),
SizedBox(height: 8),
Text(
state.tip,
style: TextStyle(
fontSize: 14,
color: Color(0xFF333333),
),
strutStyle: StrutStyle(height: 16 / 14),
),
],
);
}
return null;
}
Widget _buildWechatLogin(LoginQrCubit loginQrCubit) {
return SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
loginQrCubit.goLoginMain();
},
child: Column(
children: [
Image.asset('assets/images/login/wechat.png'),
SizedBox(height: 4),
Text(
'返回微信登录',
style: TextStyle(
fontSize: 14,
color: Color(0xFF000000),
),
),
],
),
),
],
),
);
}
}
......@@ -90,7 +90,7 @@ class WebPage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ListTile(
/*ListTile(
leading: const Icon(Icons.chat_bubble_outline),
title: const Text('消息测试'),
onTap: () {
......@@ -113,7 +113,7 @@ class WebPage extends StatelessWidget {
Navigator.pop(ctx);
ctx.read<WebCubit>().goMiniProgram();
},
),
),*/
ListTile(
leading: const Icon(Icons.refresh),
title: const Text('刷新'),
......
......@@ -10,22 +10,23 @@ class LoginPageAgreedWidget extends StatelessWidget {
children: [
TextSpan(
text: '同意',
style: TextStyle(color: Color(0xFF999999), fontSize: 14),
style: TextStyle(color: Color(0xFF999999), fontSize: 12),
),
TextSpan(
text: '《隐私保障》',
style: TextStyle(color: Color(0xFF7691FA), fontSize: 14),
style: TextStyle(color: Color(0xFF7691FA), fontSize: 12),
),
TextSpan(
text: '和',
style: TextStyle(color: Color(0xFF999999), fontSize: 14),
style: TextStyle(color: Color(0xFF999999), fontSize: 12),
),
TextSpan(
text: '《用户协议》',
style: TextStyle(color: Color(0xFF7691FA), fontSize: 14),
style: TextStyle(color: Color(0xFF7691FA), fontSize: 12),
),
],
),
strutStyle: StrutStyle(height: 13.5/12), // 设置行高为13.5
);
}
}
......@@ -96,4 +96,5 @@ flutter:
# 建议:不要直接引用 assets/ 根目录,这可能会导致打包进不必要的文件
# 最好精确到子文件夹
# - assets/dist.zip <-- 确认 zip 文件是否必须在运行时解压,这会增加包体积
- assets/images/login/
\ No newline at end of file
- assets/images/login/
- assets/images/login_v2/
\ No newline at end of file
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!