前言

登录页面作为电商应用的入口,直接影响用户的第一印象。优秀的登录设计不仅能提升用户体验,还能显著提高用户留存。本文将详细介绍如何使用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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐