完善移动端登录密码必填校验
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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('接口错误优先按错误码中文化并屏蔽未知英文', () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 功能描述:提供用户端手机号密码登录界面及本地输入校验。
|
||||
// 版本:1.1.0
|
||||
// 版本:1.2.0
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -30,9 +30,12 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
final _phone = TextEditingController();
|
||||
final _password = TextEditingController();
|
||||
final _phoneFocus = FocusNode();
|
||||
final _passwordFocus = FocusNode();
|
||||
bool _submitting = false;
|
||||
String? _error;
|
||||
String? _phoneError;
|
||||
String? _passwordError;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
@@ -47,23 +50,31 @@ class _LoginPageState extends State<LoginPage> {
|
||||
void dispose() {
|
||||
_phone.dispose();
|
||||
_password.dispose();
|
||||
_phoneFocus.dispose();
|
||||
_passwordFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 校验手机号并提交登录请求。
|
||||
/// 校验手机号和密码,通过后再提交登录请求。
|
||||
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(() {
|
||||
_submitting = true;
|
||||
_phoneError = null;
|
||||
_passwordError = null;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
@@ -123,10 +134,16 @@ class _LoginPageState extends State<LoginPage> {
|
||||
const SizedBox(height: 40),
|
||||
TextField(
|
||||
controller: _phone,
|
||||
focusNode: _phoneFocus,
|
||||
keyboardType: TextInputType.phone,
|
||||
autofillHints: const [AutofillHints.telephoneNumber],
|
||||
onChanged: (_) {
|
||||
if (_phoneError != null) setState(() => _phoneError = null);
|
||||
if (_phoneError != null || _error != null) {
|
||||
setState(() {
|
||||
_phoneError = null;
|
||||
_error = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '手机号',
|
||||
@@ -137,12 +154,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: (_) => _submitting ? null : _login(),
|
||||
decoration: InputDecoration(
|
||||
labelText: '密码',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
errorText: _passwordError,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: _obscurePassword ? '显示密码' : '隐藏密码',
|
||||
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 功能描述:验证用户端登录页字段、手机号校验和接口错误中文化。
|
||||
// 版本:1.1.0
|
||||
// 功能描述:验证用户端登录页字段、必填校验、提交行为和接口错误中文化。
|
||||
// 版本:1.2.0
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/app/dependencies.dart';
|
||||
@@ -7,6 +7,27 @@ import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/data/services/secure_session_store.dart';
|
||||
import 'package:user_app/ui/features/auth/login_page.dart';
|
||||
|
||||
/// 记录登录调用,验证本地校验是否正确阻止请求。
|
||||
class _RecordingUserSession extends UserSession {
|
||||
_RecordingUserSession() : super(SecureSessionStore());
|
||||
|
||||
int loginCallCount = 0;
|
||||
String? lastPhone;
|
||||
String? lastPassword;
|
||||
|
||||
@override
|
||||
Future<void> login({
|
||||
required String phone,
|
||||
required String password,
|
||||
String? verificationCode,
|
||||
String? requestIdentity,
|
||||
}) async {
|
||||
loginCallCount += 1;
|
||||
lastPhone = phone;
|
||||
lastPassword = password;
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证用户端登录界面的关键交互。
|
||||
void main() {
|
||||
testWidgets('login page exposes phone and password fields', (tester) async {
|
||||
@@ -18,8 +39,8 @@ void main() {
|
||||
expect(find.text('安全登录'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('手机号格式错误显示输入框中文提示', (tester) async {
|
||||
final session = UserSession(SecureSessionStore());
|
||||
testWidgets('手机号错误且密码为空时同时提示并阻止登录调用', (tester) async {
|
||||
final session = _RecordingUserSession();
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
|
||||
await tester.enterText(find.byType(TextField).first, '+8613800138000');
|
||||
@@ -27,7 +48,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 = _RecordingUserSession();
|
||||
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 = _RecordingUserSession();
|
||||
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('接口错误优先按错误码中文化并屏蔽未知英文', () {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
产品设计稿的默认登录页使用“手机号 + 验证码”方式,支持记住登录状态和忘记密码入口;用户名密码登录可作为兼容能力保留。验证码登录应具备频控、图形/行为校验和设备风控;登录前必须展示用户协议和隐私政策,并记录用户同意的协议版本。
|
||||
|
||||
手机号密码登录必须在客户端同时校验手机号格式和密码必填;任一字段无效时不得发送登录请求,应在对应字段显示中文提示并聚焦首个无效字段。登录密码须原样提交,不裁剪首尾空格,也不在登录阶段重复执行注册密码长度规则。
|
||||
|
||||
### 首次进入与安全宣导
|
||||
|
||||
- 首次登录或安全宣导内容更新后,展示可配置的“智能瓶阀控制功能介绍”,至少支持功能介绍、安全案例、法律法规和安全注意事项等内容分类。
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
### 账户、资质、上班与培训
|
||||
|
||||
- 登录支持用户名密码、记住登录状态、忘记密码、用户协议和隐私政策确认。注册页可申请配送员、安装维修员或安检员角色,但注册成功仅创建待审核账号;平台审核通过并校验资质后才可接单。
|
||||
- 手机号密码登录必须在客户端同时校验手机号格式和密码必填;任一字段无效时不得发送登录请求,应在对应字段显示中文提示并聚焦首个无效字段。登录密码须原样提交,登录按钮和密码框回车提交必须共用同一校验流程。
|
||||
- 服务人员资料包括姓名、手机号、工号、工作单位、身份证明、角色对应从业证号、证书到期日和证件照片。配送员额外维护配送车辆车牌、颜色、车型及车辆照片;安装维修/安检人员维护专业资质、培训和工具能力。证件图片默认最多 6 张,数量和类型可配置。
|
||||
- 上班/下班记录当前时间、状态、角色、地点和设备信息。未上班、超出服务区、资质失效或未完成每日培训时不得接新单;已接任务须完成、改派或按规则申请下班。
|
||||
- 每日安全知识培训在首次进入工作台时弹出,按角色题库下发单选/多选题,记录题目版本、答案、结果、完成时间和次数。未完成培训时可浏览任务但默认不能开始执行;是否允许补训由平台配置。
|
||||
|
||||
54
docs/操作日志_移动端登录密码必填校验_20260904.md
Normal file
54
docs/操作日志_移动端登录密码必填校验_20260904.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 移动端登录密码必填校验操作日志
|
||||
|
||||
操作时间:2026-09-04 22:50:49
|
||||
|
||||
操作类型:修改、扩展
|
||||
|
||||
影响模块:用户端 Flutter App 登录页、工作人员端 Flutter App 登录页及组件测试
|
||||
|
||||
## 操作前状态
|
||||
|
||||
两个 App 的手机号密码登录页只校验手机号格式。输入合法手机号并将密码留空后,页面仍调用会话登录方法并向后端发送空密码;后端执行认证比对后统一返回密码错误,页面因此显示“手机号或密码错误”,无法准确指出密码未填写。工作人员端密码框同时缺少回车提交行为。
|
||||
|
||||
## 具体操作
|
||||
|
||||
1. 两端提交函数同时计算手机号格式和密码必填状态,任一字段无效时立即返回,不调用会话登录方法。
|
||||
2. 密码字段增加“请输入密码”错误提示,手机号与密码错误可同时显示。
|
||||
3. 校验失败后聚焦首个无效字段;手机号错误优先于密码错误。
|
||||
4. 修改对应字段时清除字段错误,修改任一凭据时清除旧的接口级错误。
|
||||
5. 工作人员端密码框增加回车提交,与按钮共用同一校验函数。
|
||||
6. 密码仅判断是否为空,非空密码保持原样,不裁剪空格、不执行注册阶段的最短长度校验。
|
||||
7. 两端测试引入记录型会话替身,覆盖空密码不调用登录、双字段错误、焦点、清错、回车提交和密码原样传递。
|
||||
8. 同步用户端、工作人员端需求说明及独立项目文档。
|
||||
|
||||
## 操作后状态
|
||||
|
||||
用户端和工作人员端在空密码时均显示字段级中文提示,不再向后端发送登录请求。手机号和密码同时无效时会完整展示两项问题并定位首个错误;输入修正后提示按字段清除。按钮和密码框回车执行相同校验,合法非空密码仍按原值交给既有会话层。
|
||||
|
||||
## 代码变更
|
||||
|
||||
- `apps/user_app/lib/ui/features/auth/login_page.dart`(+32/-5,当前 244 行):密码必填、双字段校验、焦点和清错行为。
|
||||
- `apps/service_app/lib/ui/features/auth/login_page.dart`(+33/-5,当前 217 行):同等校验能力及回车提交。
|
||||
- `apps/user_app/test/ui/login_page_test.dart`(+66/-4,当前 101 行):用户端登录页回归测试。
|
||||
- `apps/service_app/test/ui/login_page_test.dart`(+61/-4,当前 96 行):工作人员端登录页回归测试。
|
||||
- `docs/03-用户端App需求.md`、`docs/04-服务端App需求.md`:同步登录校验验收口径。
|
||||
- `docs/项目文档_移动端登录密码必填校验_v1.0.md`(新增 68 行):记录结构、行为、维护方法和边界。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 用户端 `flutter analyze --no-pub`:通过,无问题。
|
||||
- 工作人员端 `flutter analyze --no-pub`:通过,无问题。
|
||||
- 用户端 `flutter test --no-pub`:通过,共 29 项测试。
|
||||
- 工作人员端 `flutter test --no-pub`:通过,共 7 项测试。
|
||||
- 两端登录页定向测试:各 5 项通过;明确验证空密码时登录调用次数为 0。
|
||||
- 两端 `flutter build web --release --no-pub --dart-define=API_BASE_URL=http://127.0.0.1:12426`:通过。
|
||||
- `git diff --check`:通过,仅提示仓库既有的 LF/CRLF 转换策略,无空白错误。
|
||||
- 本地 Release 静态服务已恢复:用户端 `5180`、工作人员端 `5181` 均返回 HTTP 200。
|
||||
- 本地后端已按开发配置恢复监听 `12426`,平台健康接口返回 HTTP 200。
|
||||
|
||||
## 风险评估
|
||||
|
||||
- 变更只发生在两个登录页面,不修改后端、会话公共接口、路由或数据库,兼容既有调用方。
|
||||
- 密码不裁剪、不转换、不提前校验长度,避免改变既有合法凭据的认证结果。
|
||||
- 空密码仍可由绕过客户端的调用方直接请求后端;后端继续使用现有统一认证失败语义,这是本次确认的范围边界。
|
||||
- 错误提示增加后会使输入框纵向高度变化,但页面位于可滚动容器内,小屏幕不会被固定高度截断。
|
||||
68
docs/项目文档_移动端登录密码必填校验_v1.0.md
Normal file
68
docs/项目文档_移动端登录密码必填校验_v1.0.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 移动端登录密码必填校验项目文档 v1.0
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
- 项目名称:用户端与工作人员端登录密码必填校验。
|
||||
- 主要功能:在手机号密码登录请求发出前完成字段级校验,阻止空密码请求并提供明确中文反馈。
|
||||
- 技术栈:Flutter、Dart、Material 组件、Flutter Widget Test。
|
||||
- 运行环境:项目当前 Flutter SDK 与 Web 构建环境。
|
||||
- 实施范围:`user_app` 与 `service_app` 登录页面及其组件测试;后端接口、会话公共接口和数据库均不修改。
|
||||
|
||||
## 2. 目录结构说明
|
||||
|
||||
```text
|
||||
platforms/
|
||||
├── apps/
|
||||
│ ├── user_app/
|
||||
│ │ ├── lib/ui/features/auth/login_page.dart # 用户端登录校验与交互
|
||||
│ │ └── test/ui/login_page_test.dart # 用户端登录组件回归测试
|
||||
│ └── service_app/
|
||||
│ ├── lib/ui/features/auth/login_page.dart # 工作人员端登录校验与交互
|
||||
│ └── test/ui/login_page_test.dart # 工作人员端登录组件回归测试
|
||||
└── docs/
|
||||
├── 03-用户端App需求.md # 用户端登录验收口径
|
||||
├── 04-服务端App需求.md # 工作人员端登录验收口径
|
||||
└── 操作日志_移动端登录密码必填校验_20260904.md # 本次实施与验证记录
|
||||
```
|
||||
|
||||
## 3. 核心文件说明
|
||||
|
||||
### 用户端登录页
|
||||
|
||||
- `_login` 同时校验手机号格式和密码是否为空;任一字段无效时立即返回,不调用 `UserSession.login`。
|
||||
- 手机号和密码各自维护字段错误,允许同时展示,并聚焦第一个无效字段。
|
||||
- 修改任一登录凭据会清除旧的接口级错误;修改对应字段会清除该字段错误。
|
||||
- 密码框回车和“安全登录”按钮共用 `_login`,密码内容不做裁剪或长度变换。
|
||||
|
||||
### 工作人员端登录页
|
||||
|
||||
- 与用户端采用相同的校验、清错和聚焦规则。
|
||||
- 密码框新增回车提交,并与“进入工作台”按钮共用 `_login`。
|
||||
- 继续调用既有 `StaffSession.login`,不改变会话及接口协议。
|
||||
|
||||
### 登录页组件测试
|
||||
|
||||
- 使用记录型会话替身统计登录调用,证明空密码时调用次数为零。
|
||||
- 覆盖双字段错误、合法手机号配空密码、首个错误字段聚焦、对应错误清除、回车提交及密码原样传递。
|
||||
|
||||
## 4. 变更记录
|
||||
|
||||
### v1.0(2026-09-04)
|
||||
|
||||
- 两个 App 新增密码必填字段错误“请输入密码”。
|
||||
- 两个 App 统一双字段校验、首错聚焦和凭据编辑清错行为。
|
||||
- 工作人员端补齐密码框回车登录。
|
||||
- 新增两端登录页回归测试,不改变后端登录错误码和既有会话接口。
|
||||
|
||||
## 5. 维护指南
|
||||
|
||||
- 后续新增登录入口时,应复用同一提交函数,禁止绕过字段校验直接调用会话登录方法。
|
||||
- 登录密码是精确凭据,不应使用 `trim`、大小写转换或注册阶段的最短长度规则处理。
|
||||
- 若未来支持验证码登录,应按登录模式分别校验密码或验证码,避免把两种凭据同时设为必填。
|
||||
- 修改交互后至少运行两端 `flutter analyze --no-pub`、登录页定向测试和完整测试。
|
||||
|
||||
## 6. 已知边界
|
||||
|
||||
- 本次仅在客户端阻止空密码请求,后端仍维持现有统一认证失败语义。
|
||||
- 本次不修改手机号规则、密码注册规则、忘记密码流程或验证码登录能力。
|
||||
- 非空但错误的密码仍由后端统一返回认证失败,客户端不会推断账户是否存在。
|
||||
Reference in New Issue
Block a user