完善移动端登录密码必填校验

This commit is contained in:
czl231
2026-09-04 23:00:55 +08:00
parent e092659622
commit 7ea39334eb
8 changed files with 317 additions and 18 deletions

View File

@@ -1,5 +1,5 @@
// 功能描述:提供工作人员端手机号密码登录界面及本地输入校验。
// 版本1.1.0
// 版本1.2.0
import 'package:flutter/material.dart';
import '../../../app/dependencies.dart';
@@ -21,25 +21,34 @@ class _LoginPageState extends State<LoginPage> {
final _phone = TextEditingController();
final _password = TextEditingController();
final _phoneFocus = FocusNode();
final _passwordFocus = FocusNode();
bool _busy = false;
String? _error;
String? _phoneError;
String? _passwordError;
bool _obscurePassword = true;
/// 校验手机号提交登录请求。
/// 校验手机号和密码,通过后再提交登录请求。
Future<void> _login() async {
final phone = _phone.text.trim();
if (!_phonePattern.hasMatch(phone)) {
final phoneValid = _phonePattern.hasMatch(phone);
final passwordValid = _password.text.isNotEmpty;
if (!phoneValid || !passwordValid) {
setState(() {
_phoneError = '请输入正确的11位手机号';
_phoneError = phoneValid ? null : '请输入正确的11位手机号';
_passwordError = passwordValid ? null : '请输入密码';
_error = null;
});
// 聚焦第一个无效字段,便于键盘和辅助技术用户立即修正。
(phoneValid ? _passwordFocus : _phoneFocus).requestFocus();
return;
}
setState(() {
_busy = true;
_phoneError = null;
_passwordError = null;
_error = null;
});
try {
@@ -59,6 +68,8 @@ class _LoginPageState extends State<LoginPage> {
void dispose() {
_phone.dispose();
_password.dispose();
_phoneFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
@@ -106,9 +117,15 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 40),
TextField(
controller: _phone,
focusNode: _phoneFocus,
keyboardType: TextInputType.phone,
onChanged: (_) {
if (_phoneError != null) setState(() => _phoneError = null);
if (_phoneError != null || _error != null) {
setState(() {
_phoneError = null;
_error = null;
});
}
},
decoration: InputDecoration(
labelText: '工作人员手机号',
@@ -119,11 +136,22 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 14),
TextField(
controller: _password,
focusNode: _passwordFocus,
obscureText: _obscurePassword,
autofillHints: const [AutofillHints.password],
onChanged: (_) {
if (_passwordError != null || _error != null) {
setState(() {
_passwordError = null;
_error = null;
});
}
},
onSubmitted: (_) => _busy ? null : _login(),
decoration: InputDecoration(
labelText: '登录密码',
prefixIcon: const Icon(Icons.lock_outline),
errorText: _passwordError,
suffixIcon: IconButton(
tooltip: _obscurePassword ? '显示密码' : '隐藏密码',
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),

View File

@@ -1,5 +1,5 @@
// 功能描述:验证工作人员端登录页字段、手机号校验和接口错误中文化。
// 版本1.1.0
// 功能描述:验证工作人员端登录页字段、必填校验、提交行为和接口错误中文化。
// 版本1.2.0
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:service_app/app/dependencies.dart';
@@ -7,6 +7,22 @@ import 'package:service_app/data/services/api_client.dart';
import 'package:service_app/data/services/secure_session_store.dart';
import 'package:service_app/ui/features/auth/login_page.dart';
/// 记录登录调用,验证本地校验是否正确阻止请求。
class _RecordingStaffSession extends StaffSession {
_RecordingStaffSession() : super(SecureSessionStore());
int loginCallCount = 0;
String? lastPhone;
String? lastPassword;
@override
Future<void> login(String phone, String password) async {
loginCallCount += 1;
lastPhone = phone;
lastPassword = password;
}
}
/// 验证工作人员端登录界面的关键交互。
void main() {
testWidgets('staff login explains the single shared entry', (tester) async {
@@ -18,8 +34,8 @@ void main() {
expect(find.byType(TextField), findsNWidgets(2));
});
testWidgets('手机号格式错误显示输入框中文提示', (tester) async {
final session = StaffSession(SecureSessionStore());
testWidgets('手机号错误且密码为空时同时提示并阻止登录调用', (tester) async {
final session = _RecordingStaffSession();
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
await tester.enterText(find.byType(TextField).first, '+8613800138000');
@@ -27,7 +43,48 @@ void main() {
await tester.pump();
expect(find.text('请输入正确的11位手机号'), findsOneWidget);
expect(find.text('请输入密码'), findsOneWidget);
expect(find.text('Invalid Argument'), findsNothing);
expect(session.loginCallCount, 0);
expect(tester.widget<TextField>(find.byType(TextField).first).focusNode?.hasFocus, isTrue);
});
testWidgets('合法手机号配空密码时提示密码必填并阻止登录调用', (tester) async {
final session = _RecordingStaffSession();
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
await tester.enterText(find.byType(TextField).first, '13800138000');
await tester.tap(find.text('进入工作台'));
await tester.pump();
expect(find.text('请输入正确的11位手机号'), findsNothing);
expect(find.text('请输入密码'), findsOneWidget);
expect(session.loginCallCount, 0);
expect(tester.widget<TextField>(find.byType(TextField).at(1)).focusNode?.hasFocus, isTrue);
});
testWidgets('修改字段清除对应错误并由回车原样提交密码', (tester) async {
final session = _RecordingStaffSession();
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
await tester.tap(find.text('进入工作台'));
await tester.pump();
await tester.enterText(find.byType(TextField).first, '13800138000');
await tester.pump();
expect(find.text('请输入正确的11位手机号'), findsNothing);
expect(find.text('请输入密码'), findsOneWidget);
await tester.enterText(find.byType(TextField).at(1), ' 密码123 ');
await tester.pump();
expect(find.text('请输入密码'), findsNothing);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
expect(session.loginCallCount, 1);
expect(session.lastPhone, '13800138000');
expect(session.lastPassword, ' 密码123 ');
});
test('接口错误优先按错误码中文化并屏蔽未知英文', () {