【Harmonyos】开源鸿蒙跨平台训练营DAY12:Flutter+鸿蒙电商实战:从零开发登录页面(含验证码倒计时、状态管理完整实现)
本文介绍了使用Flutter 3.x开发电商应用登录页面的完整实现方案。文章从项目背景出发,详细分析了包括手机号验证、验证码倒计时、协议勾选和第三方登录等核心功能需求。技术实现上,通过用户数据模型设计、API接口封装和状态管理机制,构建了完整的登录流程。重点展示了手机号输入框和验证码组件的实现细节,包括交互规范、视觉样式和错误处理机制。该方案采用模块化开发思路,代码结构清晰,可直接应用于实际项目开
前言
登录页面作为电商应用的入口,直接影响用户的第一印象。优秀的登录设计不仅能提升用户体验,还能显著提高用户留存。本文将详细介绍如何使用Flutter构建完整的登录功能,涵盖验证码倒计时、协议勾选以及第三方登录等核心模块的实现。
一、项目背景
本项目采用Flutter 3.x框架开发跨平台电商应用,主要技术架构如下:
框架版本:Flutter 3.x
页面结构:
底部导航栏主导航
包含首页、分类、购物车和个人中心四大功能模块
核心功能开发重点:构建完善的用户登录认证系统
二、功能需求分析
2.1 核心功能
以下是按照要求整理的表格形式功能模块说明:
| 功能模块 | 说明 |
|---|---|
| 手机号输入 | 支持格式验证,限制11位 |
| 验证码输入 | 6位数字验证码,60秒倒计时 |
| 登录按钮 | 防重复点击,加载状态显示 |
| 协议勾选 | 必须勾选才能登录 |
| 第三方登录 | 微信、QQ登录入口 |
2.2 开发流程
需求分析 → 数据模型设计 → API接口封装 → 登录页面开发 → 个人中心整合 → 状态管理实现 → 功能测试
三、数据模型设计
3.1 用户信息模型
文件位置:lib/viewmodels/user.dart
// 用户信息
class UserInfo {
final String id; // 用户ID
final String? nickname; // 昵称
final String? avatar; // 头像
final String phone; // 手机号
UserInfo({
required this.id,
this.nickname,
this.avatar,
required this.phone,
});
factory UserInfo.fromJSON(Map<String, dynamic> json) {
return UserInfo(
id: json['id'] ?? '',
nickname: json['nickname'],
avatar: json['avatar'],
phone: json['phone'] ?? '',
);
}
Map<String, dynamic> toJSON() {
return {
'id': id,
'nickname': nickname,
'avatar': avatar,
'phone': phone,
};
}
}
四、API接口封装
4.1 用户认证接口
文件位置:lib/api/login.dart
import 'package:harmonyos_day_four/utils/DioRequest.dart';
import 'package:harmonyos_day_four/viewmodels/user.dart';
/// 获取验证码
Future<void> getVerifyCodeAPI(String phone) async {
// 实际项目中调用真实API
// await dioRequest.post('/auth/send-code', data: {'phone': phone});
// 模拟请求延迟
await Future.delayed(const Duration(milliseconds: 500));
}
/// 登录接口
Future<UserInfo> loginAPI(String phone, String code) async {
// 实际项目中调用真实API
// final result = await dioRequest.post('/auth/login', data: {
// 'phone': phone,
// 'code': code,
// });
// return UserInfo.fromJSON(result);
// 模拟请求延迟
await Future.delayed(const Duration(seconds: 1));
// 返回模拟数据
return UserInfo(
id: '1',
nickname: '电商用户',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200',
phone: phone,
);
}
/// 退出登录
Future<void> logoutAPI() async {
await Future.delayed(const Duration(milliseconds: 500));
}
五、登录界面设计实现
5.1 页面布局
文件位置:lib/pages/login/index.dart
页面布局结构:
Scaffold
├── SafeArea
│ └── SingleChildScrollView
│ ├── Logo区域
│ ├── 欢迎文字
│ ├── 手机号输入框
│ ├── 验证码输入框(含倒计时按钮)
│ ├── 登录按钮
│ ├── 协议勾选
│ └── 第三方登录(微信、QQ)
5.2 状态管理机制
class _LoginPageState extends State<LoginPage> {
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
bool _agreeToTerms = false; // 是否同意协议
bool _isLoggingIn = false; // 是否正在登录
int _countdown = 0; // 验证码倒计时秒数
final Color _primaryColor = const Color(0xFFFF6B00);
// ...
}
5.3 手机号输入框
功能说明
用于用户输入11位中国大陆手机号码的输入框组件。
交互规范
自动弹出数字键盘
仅允许输入数字字符
实时验证输入格式(11位数字)
输入完成时自动验证有效性
视觉样式
默认显示"请输入手机号"提示文字
错误状态显示红色边框及错误提示
成功验证后显示绿色确认图标
技术实现
支持主流移动端浏览器
提供完整的API接口
包含完善的错误处理机制
Widget _buildPhoneInput() {
return Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
),
child: TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
maxLength: 11,
decoration: const InputDecoration(
hintText: '请输入手机号',
prefixIcon: Icon(Icons.phone_outlined),
counterText: '', // 隐藏字符计数器
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
),
);
}
5.4 验证码输入框与倒计时按钮
Widget _buildCodeInput() {
return Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
),
child: TextField(
controller: _codeController,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: const InputDecoration(
hintText: '请输入验证码',
prefixIcon: Icon(Icons.lock_outline),
counterText: '',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
),
),
),
const SizedBox(width: 12),
SizedBox(
width: 120,
height: 50,
child: ElevatedButton(
onPressed: _countdown > 0 ? null : _getVerifyCode,
style: ElevatedButton.styleFrom(
backgroundColor: _primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: Text(
_countdown > 0 ? '${_countdown}s' : '获取验证码',
style: const TextStyle(fontSize: 14),
),
),
),
],
);
}
5.5 验证码倒计时实现
// 获取验证码
Future<void> _getVerifyCode() async {
if (!_isValidPhone(_phoneController.text)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入正确的手机号')),
);
return;
}
try {
await getVerifyCodeAPI(_phoneController.text);
// 开始倒计时
setState(() {
_countdown = 60;
});
_startCountdown();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('验证码已发送')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('获取验证码失败: $e')),
);
}
}
// 开始倒计时
void _startCountdown() {
Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return false;
setState(() {
_countdown--;
});
return _countdown > 0;
});
}
实现要点:
采用 Future.doWhile 构建倒计时循环逻辑
每秒自动递减 _countdown 变量值
倒计时过程中禁用按钮交互(设置 onPressed: null)
添加 mounted 状态检查,避免组件卸载后内存泄漏问题
5.6 登录按钮(含加载状态)
Widget _buildLoginButton() {
return SizedBox(
height: 50,
child: ElevatedButton(
onPressed: _isLoggingIn ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: _primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: _isLoggingIn
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'登录',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
);
}
5.7 协议确认
Widget _buildAgreement() {
return Row(
children: [
SizedBox(
width: 20,
height: 20,
child: Checkbox(
value: _agreeToTerms,
onChanged: (value) {
setState(() {
_agreeToTerms = value ?? false;
});
},
fillColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return _primaryColor;
}
return Colors.grey[300];
}),
),
),
const SizedBox(width: 8),
Expanded(
child: Wrap(
children: [
const Text('我已阅读并同意',
style: TextStyle(fontSize: 12, color: Color(0xFF6B7280))),
GestureDetector(
onTap: () { /* 打开用户协议 */ },
child: const Text('《用户协议》',
style: TextStyle(fontSize: 12, color: Color(0xFFFF6B00))),
),
const Text('和', style: TextStyle(fontSize: 12)),
GestureDetector(
onTap: () { /* 打开隐私政策 */ },
child: const Text('《隐私政策》',
style: TextStyle(fontSize: 12, color: Color(0xFFFF6B00))),
),
],
),
),
],
);
}
5.8 第三方账户授权登录
Widget _buildThirdPartyLogin() {
return Column(
children: [
Row(
children: [
Expanded(child: Divider(color: Colors.grey[300])),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text('其他登录方式',
style: TextStyle(fontSize: 12, color: Colors.grey[500])),
),
Expanded(child: Divider(color: Colors.grey[300])),
],
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildThirdPartyButton(
icon: Icons.wechat,
label: '微信',
color: const Color(0xFF07C160),
onTap: _wechatLogin,
),
const SizedBox(width: 40),
_buildThirdPartyButton(
icon: Icons.chat_bubble_outline,
label: 'QQ',
color: const Color(0xFF12B7F5),
onTap: _qqLogin,
),
],
),
],
);
}
六、个人中心功能整合
6.1 游客模式
Widget _buildNotLoggedInContent() {
return Padding(
padding: const EdgeInsets.all(16),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
const Icon(Icons.login_outlined, size: 48, color: Color(0xFFFF6B00)),
const SizedBox(height: 16),
const Text('登录后享受更多服务'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _goToLogin,
child: const Text('立即登录'),
),
],
),
),
);
}
6.2 用户登录流程优化与结果处理
// 跳转到登录页面
Future<void> _goToLogin() async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
if (result != null && result is UserInfo) {
setState(() {
_userInfo = result;
});
}
}
6.3登录成功后返回数据
// 登录方法
Future<void> _login() async {
// ... 验证逻辑
try {
final result = await loginAPI(
_phoneController.text,
_codeController.text,
);
if (!mounted) return;
// 登录成功,返回用户信息
Navigator.pop(context, result);
} catch (e) {
// 错误处理
}
}
七、遇到的问题及解决方法
问题1:验证码倒计时功能出现重复触发的情况
问题描述:
用户快速点击"获取验证码"按钮多次,导致倒计时重复启动,显示异常。
原因:
没有在倒计时期间禁用按钮。
// 按钮根据倒计时状态自动禁用
ElevatedButton(
onPressed: _countdown > 0 ? null : _getVerifyCode,
// 当 _countdown > 0 时,onPressed 为 null,按钮自动禁用
child: Text(_countdown > 0 ? '${_countdown}s' : '获取验证码'),
)
**问题2:页面关闭后倒计时仍在继续
**
问题描述:
用户退出登录页面后,倒计时仍在后台运行,可能导致内存泄漏。
解决方法:
// 在倒计时循环中检查 mounted 状态
void _startCountdown() {
Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return false; // 页面已销毁,停止倒计时
setState(() {
_countdown--;
});
return _countdown > 0;
});
}
问题3:防止重复点击登录按钮
问题描述:
用户快速点击登录按钮多次,触发多次登录请求。
解决方法:
// 添加登录状态标志
bool _isLoggingIn = false;
Future<void> _login() async {
if (_isLoggingIn) return; // 防止重复点击
setState(() {
_isLoggingIn = true;
});
try {
final result = await loginAPI(...);
// ...
} finally {
if (mounted) {
setState(() {
_isLoggingIn = false;
});
}
}
}
// 按钮根据状态显示加载动画
ElevatedButton(
onPressed: _isLoggingIn ? null : _login,
child: _isLoggingIn
? CircularProgressIndicator()
: Text('登录'),
)
问题4:输入框字符计数器遮挡
问题描述:
添加 maxLength 属性会导致右下角显示字符计数器,影响界面美观度。
解决方法:
TextField(
maxLength: 11,
decoration: const InputDecoration(
counterText: '', // 隐藏字符计数器
),
)
**问题5:输入框被键盘遮挡
**
问题描述:
点击输入框时,弹出的键盘会遮挡输入框内容。
解决方法:
Scaffold(
body: SingleChildScrollView( // 使用 SingleChildScrollView
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// 输入框...
],
),
),
),
)
将内容包裹在 SingleChildScrollView 中,确保键盘弹出时仍可滚动查看完整内容。
八、页面配色方案
以下是整理后的表格形式:
颜色代码表
| 用途 | 颜色值 | 说明 |
|---|---|---|
| 主题色 | #FF6B00 | 橙色,用于按钮、高亮 |
| 输入框背景 | #F5F5F5 | 浅灰色 |
| 边框颜色 | #E5E7EB | 分割线 |
| 主文本 | #1F2937 | 深灰色 |
| 次要文本 | #6B7280 | 中灰色 |
| 微信绿 | #07C160 | 微信登录按钮 |
| QQ蓝 | #12B7F5 | QQ登录按钮 |
九、项目框架
lib/
├── api/
│ └── login.dart # 登录API接口
├── pages/
│ ├── login/
│ │ └── index.dart # 登录页面
│ └── profile/
│ └── index.dart # 个人中心(已修改)
└── viewmodels/
└── user.dart # 用户数据模型
十、完整登录流程
1. 用户点击"我的"标签
↓
2. 进入个人中心(未登录状态)
↓
3. 点击"立即登录"按钮
↓
4. 跳转到登录页面
↓
5. 输入手机号(格式验证)
↓
6. 点击"获取验证码"(60秒倒计时)
↓
7. 输入验证码
↓
8. 勾选协议
↓
9. 点击"登录"按钮
↓
10. 登录成功,返回用户信息
↓
11. 回到个人中心(已登录状态)
↓
12. 显示用户头像、昵称、订单等功能
十一、后续优化建议
以下是根据要求整理的表格:
登录功能优化项说明
| 优化项 | 说明 |
|---|---|
| 本地存储 | 使用 shared_preferences 保存登录状态 |
| Token管理 | 登录成功后保存Token,后续请求携带 |
| 自动登录 | 检查本地Token,自动恢复登录状态 |
| 验证码校验 | 实际项目中需要校验验证码正确性 |
| 第三方登录 | 集成微信、QQ官方SDK |
| 密码登录 | 添加密码登录方式作为备选 |
十二、总结
我们实现了一个功能完善的Flutter电商登录页面,主要包含以下核心功能模块:
12.1、手机号验证
采用正则表达式进行手机号格式校验
提供实时输入验证反馈
12.2、验证码机制
实现60秒倒计时功能
防止用户重复获取验证码
12.3、用户协议
强制勾选协议才能进行登录操作
12.4、交互优化
登录按钮加载动画显示
个人中心状态同步更新
12.5、第三方登录
集成微信、QQ等第三方登录入口
技术实现要点:
通过mounted检查有效避免内存泄漏问题
采用状态标志位防止重复点击
使用SingleChildScrollView解决键盘遮挡问题
通过Navigator.pop返回登录结果状态
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)