feat: 验证码登录、密码登录页UI开发
This commit is contained in:
parent
d858805563
commit
422e6a0291
20
lib/base/base_api_provider.dart
Normal file
20
lib/base/base_api_provider.dart
Normal file
@ -0,0 +1,20 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/flavors.dart';
|
||||
|
||||
class BaseApiProvider extends GetConnect {
|
||||
@override
|
||||
void onInit() {
|
||||
httpClient.baseUrl = F.apiHost;
|
||||
|
||||
// 请求拦截
|
||||
httpClient.addRequestModifier<void>((request) {
|
||||
request.headers['Authorization'] = '12345678';
|
||||
return request;
|
||||
});
|
||||
|
||||
// 响应拦截
|
||||
httpClient.addResponseModifier((request, response) {
|
||||
return response;
|
||||
});
|
||||
}
|
||||
}
|
||||
5
lib/common/enums/login_type.dart
Normal file
5
lib/common/enums/login_type.dart
Normal file
@ -0,0 +1,5 @@
|
||||
enum LoginType {
|
||||
phoneCode,
|
||||
phonePassword;
|
||||
|
||||
}
|
||||
@ -29,4 +29,21 @@ class F {
|
||||
}
|
||||
}
|
||||
|
||||
static String get apiHost {
|
||||
switch (appFlavor) {
|
||||
case Flavor.skyDev:
|
||||
return 'https://loacl.work.star-lock.cn/api';
|
||||
case Flavor.skyPre:
|
||||
return 'https://loacl.work.star-lock.cn/api';
|
||||
case Flavor.skyRelease:
|
||||
return 'https://loacl.work.star-lock.cn/api';
|
||||
case Flavor.xhjDev:
|
||||
return 'https://loacl.work.star-lock.cn/api';
|
||||
case Flavor.xhjPre:
|
||||
return 'https://loacl.work.star-lock.cn/api';
|
||||
case Flavor.xhjRelease:
|
||||
return 'https://loacl.work.star-lock.cn/api';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../flavors.dart';
|
||||
|
||||
class MyHomePage extends StatelessWidget {
|
||||
const MyHomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(F.title)),
|
||||
body: Center(child: Text('Hello ${F.title}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,12 @@ import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/routes/app_routes.dart';
|
||||
import 'package:starwork_flutter/views/home/home_binding.dart';
|
||||
import 'package:starwork_flutter/views/home/home_view.dart';
|
||||
import 'package:starwork_flutter/views/login/forgotPassword/forgot_password_binding.dart';
|
||||
import 'package:starwork_flutter/views/login/forgotPassword/forgot_password_view.dart';
|
||||
import 'package:starwork_flutter/views/login/forgotPassword/setNewPassword/set_new_password_binding.dart';
|
||||
import 'package:starwork_flutter/views/login/forgotPassword/setNewPassword/set_new_password_view.dart';
|
||||
import 'package:starwork_flutter/views/login/inputVerificationCode/input_verification_code_binding.dart';
|
||||
import 'package:starwork_flutter/views/login/inputVerificationCode/input_verification_code_view.dart';
|
||||
import 'package:starwork_flutter/views/login/login_binding.dart';
|
||||
import 'package:starwork_flutter/views/login/login_view.dart';
|
||||
import 'package:starwork_flutter/views/main/main_binding.dart';
|
||||
@ -39,5 +45,20 @@ class AppPages {
|
||||
page: () => const MineView(),
|
||||
binding: MineBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: AppRoutes.inputVerificationCode,
|
||||
page: () => const InputVerificationCodeView(),
|
||||
binding: InputVerificationCodeBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: AppRoutes.forgotPassword,
|
||||
page: () => const ForgotPasswordView(),
|
||||
binding: ForgotPasswordBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: AppRoutes.setNewPassword,
|
||||
page: () => const SetNewPasswordView(),
|
||||
binding: SetNewPasswordBinding(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@ -5,4 +5,7 @@ class AppRoutes{
|
||||
static const String main = '/main';
|
||||
static const String messages = '/messages';
|
||||
static const String mine = '/mine';
|
||||
static const String inputVerificationCode = '/inputVerificationCode';
|
||||
static const String forgotPassword = '/forgotPassword';
|
||||
static const String setNewPassword = '/setNewPassword';
|
||||
}
|
||||
11
lib/views/login/forgotPassword/forgot_password_binding.dart
Normal file
11
lib/views/login/forgotPassword/forgot_password_binding.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/views/login/forgotPassword/forgot_password_controller.dart';
|
||||
|
||||
class ForgotPasswordBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ForgotPasswordController>(
|
||||
() => ForgotPasswordController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/base/base_controller.dart';
|
||||
import 'package:starwork_flutter/routes/app_routes.dart';
|
||||
import 'package:starwork_flutter/views/login/login_controller.dart';
|
||||
|
||||
class ForgotPasswordController extends BaseController {
|
||||
// 在任意位置获取已注入的实例
|
||||
final loginController = Get.find<LoginController>();
|
||||
|
||||
// 获取手机验证码
|
||||
void requestPhoneCode() {
|
||||
Get.toNamed(
|
||||
AppRoutes.inputVerificationCode,
|
||||
parameters: {
|
||||
'phone': loginController.phoneController.text.trim(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
102
lib/views/login/forgotPassword/forgot_password_view.dart
Normal file
102
lib/views/login/forgotPassword/forgot_password_view.dart
Normal file
@ -0,0 +1,102 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/views/login/forgotPassword/forgot_password_controller.dart';
|
||||
|
||||
class ForgotPasswordView extends GetView<ForgotPasswordController> {
|
||||
const ForgotPasswordView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'请输入手机号'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20.h,
|
||||
),
|
||||
TextField(
|
||||
controller: controller.loginController.phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLength: 11,
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
hintText: '请输入手机号码'.tr,
|
||||
border: const UnderlineInputBorder(),
|
||||
suffixIcon: controller.loginController.phoneController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, color: Colors.grey),
|
||||
onPressed: () {
|
||||
controller.loginController.phoneController.clear(); // 清空输入
|
||||
},
|
||||
)
|
||||
: null,
|
||||
// 如果没有内容,就不显示清除按钮,
|
||||
// 获取焦点时的边框
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: controller.loginController.isFormValid.value
|
||||
? Colors.blue
|
||||
: Colors.blue.withOpacity(0.5),
|
||||
), //
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 48.h,
|
||||
),
|
||||
Obx(
|
||||
() => ElevatedButton(
|
||||
onPressed: controller.loginController.isFormValid.value
|
||||
? controller.requestPhoneCode
|
||||
: null,
|
||||
style: ButtonStyle(
|
||||
minimumSize:
|
||||
MaterialStateProperty.all(Size(double.infinity, 44.h)),
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
controller.loginController.isFormValid.value
|
||||
? Colors.blue
|
||||
: Colors.blue.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'获取验证码'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/views/login/forgotPassword/setNewPassword/set_new_password_controller.dart';
|
||||
|
||||
class SetNewPasswordBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<SetNewPasswordController>(
|
||||
() => SetNewPasswordController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/base/base_controller.dart';
|
||||
|
||||
class SetNewPasswordController extends BaseController {
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
TextEditingController newPasswordController = TextEditingController();
|
||||
final isPasswordVisible = true.obs;
|
||||
final isNewPasswordVisible = true.obs;
|
||||
final isFormValid = false.obs;
|
||||
final isNewFormValid = false.obs;
|
||||
final isAllFormValid = false.obs;
|
||||
|
||||
void setNewPassword(){
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,239 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/views/login/forgotPassword/setNewPassword/set_new_password_controller.dart';
|
||||
|
||||
class SetNewPasswordView extends GetView<SetNewPasswordController> {
|
||||
const SetNewPasswordView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'设置新密码'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20.h,
|
||||
),
|
||||
TextField(
|
||||
controller: controller.passwordController,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
textInputAction: TextInputAction.done,
|
||||
obscureText: controller.isPasswordVisible.value,
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
hintText: '请输入新密码'.tr,
|
||||
border: const UnderlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isPasswordVisible.value
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () => controller.isPasswordVisible.value =
|
||||
!controller.isPasswordVisible.value,
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: controller.isFormValid.value
|
||||
? Colors.blue
|
||||
: Colors.blue.withOpacity(0.5),
|
||||
), //
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20.h,
|
||||
),
|
||||
TextField(
|
||||
controller: controller.newPasswordController,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
textInputAction: TextInputAction.done,
|
||||
obscureText: controller.isNewPasswordVisible.value,
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
hintText: '再次输入新密码'.tr,
|
||||
border: const UnderlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isNewPasswordVisible.value
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () => controller.isNewPasswordVisible.value =
|
||||
!controller.isNewPasswordVisible.value,
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: controller.isNewFormValid.value
|
||||
? Colors.blue
|
||||
: Colors.blue.withOpacity(0.5),
|
||||
), //
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20.h,
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(
|
||||
14.r,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
activeColor: Colors.blue,
|
||||
onChanged: (value) {},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'8-16位,由大写字母、小写字母、数字、特殊字符任意两种及以上组成',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black45,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
activeColor: Colors.blue,
|
||||
onChanged: (value) {},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'连续相同的字符不能超过4个',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black45,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
activeColor: Colors.blue,
|
||||
onChanged: (value) {},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'连续的数字不能超过4个',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black45,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
activeColor: Colors.blue,
|
||||
onChanged: (value) {},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'不能使用安全性低的密码',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black45,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 48.h,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: controller.isAllFormValid.value
|
||||
? controller.setNewPassword
|
||||
: null,
|
||||
style: ButtonStyle(
|
||||
minimumSize:
|
||||
MaterialStateProperty.all(Size(double.infinity, 44.h)),
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
controller.isFormValid.value
|
||||
? Colors.blue
|
||||
: Colors.blue.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'提交'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/views/login/inputVerificationCode/input_verification_code_controller.dart';
|
||||
|
||||
class InputVerificationCodeBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<InputVerificationCodeController>(
|
||||
() => InputVerificationCodeController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/base/base_controller.dart';
|
||||
import 'package:starwork_flutter/routes/app_routes.dart';
|
||||
|
||||
class InputVerificationCodeController extends BaseController {
|
||||
var phone = ''.obs;
|
||||
var previousRoute = ''.obs;
|
||||
|
||||
// 是否正在倒计时
|
||||
var isCountingDown = false.obs;
|
||||
|
||||
// 倒计时剩余秒数
|
||||
var countdownValue = 60.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
final params = Get.parameters;
|
||||
String? phoneParam = params['phone'];
|
||||
// 在目标页面获取来源路由
|
||||
previousRoute.value = Get.previousRoute; // 如 "/login"
|
||||
|
||||
if (phoneParam != null && phoneParam.isNotEmpty) {
|
||||
phone.value = maskMiddleFive(phoneParam);
|
||||
// 开始倒计时(比如60秒)
|
||||
startCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
String maskMiddleFive(String phone) {
|
||||
if (phone.length < 8) return phone; // 如果手机号少于8位,不处理(或根据需求返回原值/报错)
|
||||
|
||||
// 假设手机号至少有 8 位,我们保留前 3 位,中间 5 位替换为 ****,显示后 3 位
|
||||
// 13812345678 → 138****678
|
||||
String firstPart = phone.substring(0, 3); // 前3位:138
|
||||
String middleReplaced = '*****'; // 中间5位替换为 ****
|
||||
String lastPart = phone.substring(8); // 后3位:678
|
||||
|
||||
return '$firstPart$middleReplaced$lastPart'; // 拼接:138****678
|
||||
}
|
||||
|
||||
/// 开始倒计时(比如60秒)
|
||||
void startCountdown() {
|
||||
if (isCountingDown.value) return; // 避免重复启动
|
||||
|
||||
isCountingDown.value = true;
|
||||
countdownValue.value = 60;
|
||||
|
||||
// 使用 Timer 实现倒计时
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (countdownValue.value > 1) {
|
||||
countdownValue.value--;
|
||||
} else {
|
||||
timer.cancel(); // 停止计时器
|
||||
isCountingDown.value = false;
|
||||
countdownValue.value = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void requestPhoneCodeLogin(String code) {
|
||||
if (previousRoute.value.contains(AppRoutes.login)) {
|
||||
_handleLogin();
|
||||
} else if (previousRoute.value.contains(AppRoutes.forgotPassword)) {
|
||||
_handleForgotPassword();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLogin() {
|
||||
Get.offAndToNamed(AppRoutes.main);
|
||||
}
|
||||
|
||||
void _handleForgotPassword() {
|
||||
Get.toNamed(AppRoutes.setNewPassword);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pinput/pinput.dart';
|
||||
import 'package:starwork_flutter/views/login/inputVerificationCode/input_verification_code_controller.dart';
|
||||
|
||||
class InputVerificationCodeView
|
||||
extends GetView<InputVerificationCodeController> {
|
||||
const InputVerificationCodeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
width: 1.sw,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 40.w,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"请输入验证码",
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${'已发送验证码至'.tr}${controller.phone.value}',
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => _buildRequestCodeButton(),
|
||||
),
|
||||
SizedBox(
|
||||
height: 32.h,
|
||||
),
|
||||
// Pinput 验证码输入框
|
||||
Pinput(
|
||||
length: 6,
|
||||
obscureText: false,
|
||||
defaultPinTheme: PinTheme(
|
||||
width: 50.w,
|
||||
height: 50.w,
|
||||
textStyle: TextStyle(fontSize: 20.sp, color: Colors.black),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) return '请输入验证码';
|
||||
if (value.length != 6) return '验证码需6位';
|
||||
return null;
|
||||
},
|
||||
onCompleted: (pin) {
|
||||
controller.requestPhoneCodeLogin(pin);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildRequestCodeButton() {
|
||||
if (controller.isCountingDown.value) {
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '如未收到,请'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '${controller.countdownValue.value}s'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '后重新获取'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.startCountdown();
|
||||
},
|
||||
child: Text(
|
||||
'重新获取验证码'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,31 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/base/base_controller.dart';
|
||||
import 'package:starwork_flutter/common/enums/login_type.dart';
|
||||
import 'package:starwork_flutter/routes/app_pages.dart';
|
||||
import 'package:starwork_flutter/routes/app_routes.dart';
|
||||
|
||||
class LoginController extends BaseController {
|
||||
int phoneNumberSize = 11;
|
||||
|
||||
TextEditingController phoneController = TextEditingController();
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
|
||||
final isFormValid = false.obs;
|
||||
final isPasswordVisible = true.obs;
|
||||
final isPrivacyAgreementValid = 0.obs;
|
||||
final loginType = LoginType.phoneCode.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// 监听输入变化
|
||||
phoneController.addListener(_validateForm);
|
||||
|
||||
phoneController.text = '18269109817';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -31,7 +42,158 @@ class LoginController extends BaseController {
|
||||
|
||||
// 获取手机验证码
|
||||
void requestPhoneCode() {
|
||||
debugPrint("获取手机验证码");
|
||||
showToast("获取手机验证码");
|
||||
if (isPrivacyAgreementValid.value != 1) {
|
||||
_showCustomDialog(
|
||||
title: '欢迎使用星勤'.tr,
|
||||
content: '1',
|
||||
onConfirm: () {
|
||||
isPrivacyAgreementValid.value = 1;
|
||||
Get.toNamed(
|
||||
AppRoutes.inputVerificationCode,
|
||||
parameters: {
|
||||
'phone': phoneController.text.trim(),
|
||||
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
Get.toNamed(
|
||||
AppRoutes.inputVerificationCode,
|
||||
parameters: {
|
||||
'phone': phoneController.text.trim(),
|
||||
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showCustomDialog({
|
||||
required String title,
|
||||
required String content,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14.r), // 圆角
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 22.h),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(22.w),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '请你阅读并同意'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '《用户协议》'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '和'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '《隐私政策》'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Colors.grey, // 右侧边框颜色
|
||||
width: 0.5, // 右侧边框宽度
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
// 左侧文本占一半
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 42.h,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: Colors.grey, // 右侧边框颜色
|
||||
width: 0.5, // 右侧边框宽度
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'取消'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
// 右侧文本占一半
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
onConfirm();
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 42.h,
|
||||
child: Text(
|
||||
'同意并继续'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/common/enums/login_type.dart';
|
||||
import 'package:starwork_flutter/routes/app_routes.dart';
|
||||
|
||||
import 'login_controller.dart';
|
||||
|
||||
@ -15,6 +19,7 @@ class LoginView extends GetView<LoginController> {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: _buildBody(),
|
||||
@ -25,21 +30,28 @@ class LoginView extends GetView<LoginController> {
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return SizedBox(
|
||||
height: 0.9.sh,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildTop(),
|
||||
_buildPrivacyPolicy(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildTop() {
|
||||
return Container(
|
||||
height: 1.sh,
|
||||
margin: EdgeInsets.symmetric(vertical: 48.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 32.w),
|
||||
margin: EdgeInsets.symmetric(horizontal: 40.w, vertical: 40.h),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTitle(),
|
||||
SizedBox(
|
||||
height: 32.h,
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
_buildPhoneInputAndLoginButton(),
|
||||
SizedBox(
|
||||
height: 32.h,
|
||||
),
|
||||
_buildPrivacyPolicy(),
|
||||
SizedBox(height: 20.h),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -58,9 +70,6 @@ class LoginView extends GetView<LoginController> {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 4.h,
|
||||
),
|
||||
Text(
|
||||
'未注册手机号验证后将自动创建账号'.tr,
|
||||
style: TextStyle(
|
||||
@ -87,6 +96,15 @@ class LoginView extends GetView<LoginController> {
|
||||
counterText: '',
|
||||
hintText: '请输入手机号码'.tr,
|
||||
border: const UnderlineInputBorder(),
|
||||
suffixIcon: controller.phoneController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, color: Colors.grey),
|
||||
onPressed: () {
|
||||
controller.phoneController.clear(); // 清空输入
|
||||
},
|
||||
)
|
||||
: null,
|
||||
// 如果没有内容,就不显示清除按钮,
|
||||
// 获取焦点时的边框
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
@ -94,12 +112,47 @@ class LoginView extends GetView<LoginController> {
|
||||
? Colors.blue
|
||||
: Colors.blue.withOpacity(0.5),
|
||||
), //
|
||||
// 🔥 你想要的颜色
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: controller.loginType.value == LoginType.phonePassword,
|
||||
child: SizedBox(
|
||||
height: 18.h,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: controller.loginType.value == LoginType.phonePassword,
|
||||
child: TextField(
|
||||
controller: controller.passwordController,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
textInputAction: TextInputAction.done,
|
||||
obscureText: controller.isPasswordVisible.value,
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
hintText: '请输入密码'.tr,
|
||||
border: const UnderlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isPasswordVisible.value
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () => controller.isPasswordVisible.value =
|
||||
!controller.isPasswordVisible.value,
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: controller.isFormValid.value
|
||||
? Colors.blue
|
||||
: Colors.blue.withOpacity(0.5),
|
||||
), //
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 24.h,
|
||||
height: 48.h,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: controller.isFormValid.value
|
||||
@ -120,7 +173,9 @@ class LoginView extends GetView<LoginController> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'获取验证码'.tr,
|
||||
controller.loginType.value == LoginType.phoneCode
|
||||
? '获取验证码'.tr
|
||||
: '登录'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
@ -128,35 +183,157 @@ class LoginView extends GetView<LoginController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 22.h,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
'密码登录'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
SizedBox(height: 20.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: LoginType.phoneCode == controller.loginType.value,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
controller.loginType.value = LoginType.phonePassword;
|
||||
},
|
||||
child: Text(
|
||||
'密码登录'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: LoginType.phonePassword == controller.loginType.value,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
controller.loginType.value = LoginType.phoneCode;
|
||||
},
|
||||
child: Text(
|
||||
'验证码登录'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: LoginType.phonePassword == controller.loginType.value,
|
||||
child: Text(
|
||||
'|',
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: LoginType.phonePassword == controller.loginType.value,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor: MaterialStateProperty.all(Colors.transparent), // 墨水覆盖
|
||||
side: MaterialStateProperty.all(null), // 取消边框
|
||||
),
|
||||
onPressed: () {
|
||||
Get.toNamed(
|
||||
AppRoutes.forgotPassword,
|
||||
parameters: {
|
||||
'phone': controller.phoneController.text,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'忘记密码'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_handleLoginButtonText() {
|
||||
if (controller.loginType.value == LoginType.phoneCode) {
|
||||
return;
|
||||
} else if (controller.loginType.value == LoginType.phonePassword) {
|
||||
return '验证码登录'.tr + ' | '.tr + '忘记密码'.tr;
|
||||
}
|
||||
}
|
||||
|
||||
_buildPrivacyPolicy() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Radio(
|
||||
value: '1',
|
||||
groupValue: 1,
|
||||
onChanged: (value) {},
|
||||
),
|
||||
],
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.isPrivacyAgreementValid.value =
|
||||
controller.isPrivacyAgreementValid.value == 1 ? 0 : 1;
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Obx(
|
||||
() => Checkbox(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
activeColor: Colors.blue,
|
||||
value: controller.isPrivacyAgreementValid.value == 1,
|
||||
// 转换为 bool
|
||||
onChanged: (bool? value) {
|
||||
controller.isPrivacyAgreementValid.value =
|
||||
value == true ? 1 : 0;
|
||||
},
|
||||
),
|
||||
),
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '我已阅读并同意'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '《用户协议》'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '和'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '《隐私政策》'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
16
pubspec.lock
16
pubspec.lock
@ -341,6 +341,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
pinput:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pinput
|
||||
sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -490,6 +498,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_platform
|
||||
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -27,6 +27,8 @@ dependencies:
|
||||
flutter_screenutil: ^5.9.3
|
||||
# 提示
|
||||
fluttertoast: ^8.2.8
|
||||
# 验证码输入框
|
||||
pinput: ^5.0.1
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user