Flutter开源鸿蒙跨平台训练营 Day 13从零开发注册页面

Flutter电商实战:从零开发注册页面

前言

在电商类移动应用的产品体系中,注册页面是用户触达产品核心功能的第一道入口,其设计的简洁性、交互的流畅性直接影响用户注册转化率与产品初始体验。本文基于Flutter 3.x跨平台技术栈,从零搭建一套完整的电商应用注册页面,实现手机号验证、60秒验证码倒计时、密码强度校验、确认密码一致性验证等核心功能,同时完成注册页面与登录页面的双向跳转集成,为Flutter跨平台电商项目提供可直接复用的注册功能模块。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


一、项目背景

本开发实战基于Flutter 3.x 版本进行开发,是已完成的电商应用登录页面功能的延伸开发,技术栈聚焦Flutter原生开发,适配鸿蒙等跨平台终端场景。

开发核心目标:实现一套高可用、高体验的用户注册功能,包含完整的前端表单验证、接口封装、页面交互逻辑,与现有登录页面形成功能闭环,满足电商应用的用户账号体系搭建需求。

二、功能需求分析

2.1 核心功能

注册页面作为用户账号创建的核心载体,需实现表单验证、交互反馈、页面跳转等全流程功能,各模块具体要求如下:

功能模块 详细说明
手机号输入 支持11位国内手机号格式正则验证,非合规格式实时反馈
验证码输入 仅支持6位数字验证码,点击获取后触发60秒倒计时,倒计时期间按钮置灰
密码输入 密码长度限制6-20位,必须同时包含字母和数字,不允许特殊字符,支持密码显示/隐藏切换
确认密码 与密码输入框内容实时校验,确保两次输入一致,同样支持显示/隐藏
注册按钮 实现防重复点击机制,点击后进入加载状态,验证不通过时按钮禁用
协议勾选 为必选项,未勾选时无法触发注册操作,关联用户协议与隐私政策
已有账号跳转 提供清晰的登录入口,点击快速返回登录页面

2.2 开发流程

为保证开发的逻辑性和可维护性,本次注册页面开发遵循标准化流程,各阶段环环相扣,具体流程如下:
需求分析 → 密码验证规则设计 → API接口封装 → 注册页面UI实现 → 登录页面集成 → 功能测试与问题修复

三、密码验证规则设计

3.1 密码强度要求

密码作为用户账号的核心安全保障,需制定严格且易执行的验证规则,本次设计的密码规则兼顾安全性与易用性,同时通过正则表达式实现前端快速校验。

密码核心规则

  1. 长度限制:6-20位字符;
  2. 字符组成:必须同时包含至少一个英文字母(大小写均可)和至少一个数字;
  3. 字符限制:不允许包含任何特殊字符(如@、#、$等)。

前端验证代码实现

dart 复制代码
// 验证密码格式合法性
bool _isValidPassword(String password) {
  // 正则表达式匹配:6-20位,含字母+数字,无特殊字符
  return RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,20}$')
      .hasMatch(password);
}

密码验证示例测试

密码示例 验证结果 失败/通过原因
abc123 ✅ 通过 6位字符,同时包含字母和数字
abcd1234 ✅ 通过 8位字符,同时包含字母和数字
123456 ❌ 不通过 仅包含数字,缺少字母
abcdef ❌ 不通过 仅包含字母,缺少数字
abc ❌ 不通过 字符长度不足6位
Abc12345678901234567890 ❌ 不通过 字符长度超过20位

四、API接口封装

注册功能涉及获取验证码提交注册信息两个核心接口,本次开发将接口统一封装在独立文件中,实现业务逻辑与页面逻辑的解耦,便于后续接口维护和接口地址统一管理。

4.1 注册相关接口

文件路径lib/api/register.dart
接口封装代码

dart 复制代码
import 'package:harmonyos_day_four/utils/DioRequest.dart';

/// 获取注册验证码接口
/// [phone]:用户输入的11位手机号
Future<void> getRegisterCodeAPI(String phone) async {
  // 实际项目中替换为真实后端接口地址
  // await dioRequest.post('/auth/register/send-code', data: {'phone': phone});

  // 模拟网络请求延迟,适配前端交互体验
  await Future.delayed(const Duration(milliseconds: 500));
}

/// 提交注册信息接口
/// [phone]:手机号 | [code]:6位验证码 | [password]:加密后的用户密码
Future<void> registerAPI(String phone, String code, String password) async {
  // 实际项目中替换为真实后端接口地址,建议密码做加密处理后传输
  // await dioRequest.post('/auth/register', data: {
  //   'phone': phone,
  //   'code': code,
  //   'password': password,
  // });

  // 模拟网络请求延迟,适配前端加载状态
  await Future.delayed(const Duration(seconds: 1));
}

接口设计说明

  1. 基于Dio网络请求框架封装,统一请求拦截和响应处理;
  2. 预留真实接口调用代码,注释中明确参数传递规则;
  3. 添加模拟延迟,避免前端无感知的快速请求完成,提升用户交互体验;
  4. 实际项目中,密码参数需通过MD5、RSA等方式加密后再传输,保障数据安全。

五、注册页面UI实现

注册页面的UI实现遵循简洁易用的设计原则,采用模块化开发方式,将页面拆分为多个独立组件,便于后续维护和功能扩展,同时通过状态管理实现表单数据的实时监控和交互状态的动态更新。

5.1 页面结构

文件路径lib/pages/register/index.dart

页面基于Flutter的Scaffold脚手架搭建,使用SingleChildScrollView适配小屏设备,防止键盘弹出导致表单被遮挡,整体布局结构如下:

复制代码
Scaffold
├── SafeArea(适配状态栏/导航栏)
│   └── SingleChildScrollView(滚动布局,适配小屏)
│       ├── 返回按钮(左上角)
│       ├── Logo和注册标题(页面头部)
│       ├── 手机号输入框(带格式验证)
│       ├── 验证码输入框(含倒计时按钮)
│       ├── 密码输入框(含显示/隐藏切换)
│       ├── 确认密码输入框(含显示/隐藏切换)
│       ├── 注册按钮(防重复点击,加载状态)
│       ├── 协议勾选框(必选,关联协议文本)
│       └── 已有账号跳转入口(页面底部)

5.2 状态管理

页面采用Flutter原生的StatefulWidget实现状态管理,定义表单相关的控制器和状态变量,实现输入框数据绑定、交互状态切换(如密码显示/隐藏、倒计时、加载状态等),核心状态代码如下:

dart 复制代码
class _RegisterPageState extends State<RegisterPage> {
  // 输入框控制器:绑定手机号、验证码、密码、确认密码
  final TextEditingController _phoneController = TextEditingController();
  final TextEditingController _codeController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  final TextEditingController _confirmPasswordController = TextEditingController();

  bool _agreeToTerms = false;         // 是否同意用户协议和隐私政策
  bool _isRegistering = false;        // 是否正在执行注册请求(加载状态)
  int _countdown = 0;                 // 验证码倒计时秒数,0为未开始
  bool _showPassword = false;         // 密码输入框是否显示明文
  bool _showConfirmPassword = false;  // 确认密码输入框是否显示明文

  // 项目主题色:橙色(用于按钮、链接、高亮文字)
  final Color _primaryColor = const Color(0xFFFF6B00);
  // ... 其他业务逻辑代码
}

5.3 返回按钮

返回按钮位于页面左上角,采用圆形灰色背景设计,点击后返回上一级登录页面,实现代码如下:

dart 复制代码
Widget _buildBackButton() {
  return GestureDetector(
    onTap: () => Navigator.pop(context), // 返回到上一个页面
    child: Container(
      width: 40,
      height: 40,
      decoration: BoxDecoration(
        color: Colors.grey[100],
        shape: BoxShape.circle, // 圆形按钮
      ),
      child: const Icon(Icons.arrow_back, size: 20), // 返回箭头图标
    ),
  );
}

5.4 密码输入框(含显示/隐藏)

密码输入框采用浅灰色背景圆角设计,左侧添加锁形图标,右侧添加眼睛图标实现明文/密文 切换,核心实现通过obscureText属性控制,代码如下:

dart 复制代码
Widget _buildPasswordInput() {
  return Container(
    decoration: BoxDecoration(
      color: Colors.grey[100],
      borderRadius: BorderRadius.circular(12), // 圆角设计,提升美观度
    ),
    child: TextField(
      controller: _passwordController, // 绑定密码控制器
      obscureText: !_showPassword,     // true为密文,false为明文
      decoration: InputDecoration(
        hintText: '请输入密码(6-20位,含字母和数字)', // 明确密码规则提示
        prefixIcon: const Icon(Icons.lock_outline), // 左侧锁形图标
        suffixIcon: GestureDetector(
          onTap: () {
            // 点击眼睛图标,切换密码显示状态
            setState(() {
              _showPassword = !_showPassword;
            });
          },
          child: Icon(
            // 根据状态切换眼睛图标
            _showPassword
                ? Icons.visibility_outlined    // 明文:睁眼图标
                : Icons.visibility_off_outlined, // 密文:闭眼图标
            color: Colors.grey[400],
          ),
        ),
        border: InputBorder.none, // 隐藏输入框默认边框
        contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), // 内边距适配
      ),
    ),
  );
}

5.5 确认密码输入框

确认密码输入框的UI设计与密码输入框保持一致,同样支持显示/隐藏切换,仅修改提示文字和绑定的控制器,实现代码如下:

dart 复制代码
Widget _buildConfirmPasswordInput() {
  return Container(
    decoration: BoxDecoration(
      color: Colors.grey[100],
      borderRadius: BorderRadius.circular(12),
    ),
    child: TextField(
      controller: _confirmPasswordController, // 绑定确认密码控制器
      obscureText: !_showConfirmPassword,     // 控制显示/隐藏
      decoration: InputDecoration(
        hintText: '请确认密码', // 确认密码提示文字
        prefixIcon: const Icon(Icons.lock_outline),
        suffixIcon: GestureDetector(
          onTap: () {
            setState(() {
              _showConfirmPassword = !_showConfirmPassword;
            });
          },
          child: Icon(
            _showConfirmPassword
                ? Icons.visibility_outlined
                : Icons.visibility_off_outlined,
            color: Colors.grey[400],
          ),
        ),
        border: InputBorder.none,
        contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
      ),
    ),
  );
}

5.6 注册验证逻辑

注册按钮点击后,触发全量表单验证 ,验证通过后才会调用注册接口,验证失败则通过SnackBar给出明确的错误提示,实现防错友好反馈,完整的注册验证逻辑代码如下:

dart 复制代码
Future<void> _register() async {
  // 1. 验证手机号:非空且格式合规
  if (!_isValidPhone(_phoneController.text)) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请输入正确的11位手机号')),
    );
    return;
  }

  // 2. 验证验证码:非空
  if (_codeController.text.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请输入6位数字验证码')),
    );
    return;
  }

  // 3. 验证密码:非空且格式合规
  if (_passwordController.text.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请输入密码')),
    );
    return;
  }
  if (!_isValidPassword(_passwordController.text)) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('密码长度6-20位,必须包含字母和数字')),
    );
    return;
  }

  // 4. 验证确认密码:与密码一致
  if (_confirmPasswordController.text != _passwordController.text) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('两次密码输入不一致,请重新输入')),
    );
    return;
  }

  // 5. 验证协议勾选:必须勾选
  if (!_agreeToTerms) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请先同意用户协议和隐私政策')),
    );
    return;
  }

  // 6. 验证通过,执行注册请求
  setState(() {
    _isRegistering = true; // 进入加载状态,按钮置灰
  });

  try {
    // 调用注册接口,提交表单数据
    await registerAPI(
      _phoneController.text,
      _codeController.text,
      _passwordController.text,
    );
    if (!mounted) return; // 防止页面销毁后执行状态更新

    // 注册成功:给出提示,返回登录页面
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('注册成功,请使用新账号登录')),
    );
    Navigator.pop(context);
  } catch (e) {
    if (!mounted) return;
    // 注册失败:给出错误提示
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('注册失败:$e')),
    );
  } finally {
    if (mounted) {
      // 无论成功/失败,结束加载状态
      setState(() {
        _isRegistering = false;
      });
    }
  }
}

验证逻辑设计要点

  1. 验证顺序遵循从简单到复杂,减少不必要的逻辑执行;
  2. 每个验证节点失败后立即返回,并给出明确、具体的错误提示;
  3. 通过mounted判断页面是否存在,防止页面销毁后执行状态更新导致崩溃;
  4. 注册请求通过try-catch-finally包裹,确保加载状态无论成功失败都能恢复。

5.7 已有账号跳转

在页面底部添加已有账号?立即登录的跳转入口,橙色高亮"立即登录"文字,与项目主题色保持一致,点击后返回登录页面,实现代码如下:

dart 复制代码
Widget _buildLoginLink() {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center, // 水平居中
    children: [
      Text(
        '已有账号?',
        style: TextStyle(fontSize: 14, color: Colors.grey[600]), // 灰色次要文字
      ),
      GestureDetector(
        onTap: _goToLogin, // 绑定跳转方法
        child: const Text(
          '立即登录',
          style: TextStyle(
            fontSize: 14,
            color: Color(0xFFFF6B00), // 主题橙色
            fontWeight: FontWeight.w600, // 加粗高亮
          ),
        ),
      ),
    ],
  );
}

// 跳转到登录页面
void _goToLogin() {
  Navigator.pop(context); // 返回到上一级登录页面
}

六、登录页面集成

注册功能并非独立存在,需与已完成的登录页面实现双向跳转 ,在登录页面添加去注册入口,点击后跳转到注册页面,完成功能闭环。

6.1 添加注册入口

文件路径lib/pages/login/index.dart
集成步骤与代码

  1. 导入注册页面组件;
  2. 定义跳转方法;
  3. 在登录页面合适位置添加跳转入口UI。
dart 复制代码
// 1. 导入注册页面组件
import 'package:harmonyos_day_four/pages/register/index.dart';

// 2. 定义跳转到注册页面的方法
void _goToRegister() {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => const RegisterPage()),
  );
}

// 3. 在登录页面添加跳转入口UI(示例:放在页面底部欢迎文字位置)
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Text('还没有账号?', style: TextStyle(color: Colors.grey[600])),
    GestureDetector(
      onTap: _goToRegister, // 绑定跳转方法
      child: const Text(
        '去注册',
        style: TextStyle(
          color: Color(0xFFFF6B00), // 与注册页面统一主题色
          fontWeight: FontWeight.w600,
        ),
      ),
    ),
  ],
)

七、开发问题及解决方法

在注册页面的开发和测试过程中,针对用户体验和功能安全性的问题,逐一进行优化解决,确保页面的可用性和安全性,以下为核心问题及对应的解决方案:

问题1:密码明文显示存在安全隐患

问题描述 :密码输入框默认明文显示,用户在公共场景输入时,密码易被他人窥视,存在账号安全隐患。
解决方法 :通过obscureText属性将密码默认设为密文,添加眼睛图标实现手动切换,用户可根据自身需求选择是否显示明文,核心代码见5.4节密码输入框实现。

问题2:用户易输错密码,导致注册失败

问题描述 :密码为密文输入,用户容易出现输入错误,且无法直观发现,导致注册时两次密码不一致。
解决方法 :添加确认密码输入框,注册前严格校验两次输入的密码是否一致,不一致时给出明确提示,核心验证代码见5.6节注册验证逻辑第4点。

问题3:密码规则提示不清晰,用户多次输入错误

问题描述 :用户不清楚密码的格式要求,反复输入不符合规则的密码,导致注册体验变差。
解决方法 :在密码输入框的hintText明确标注密码规则(6-20位,含字母和数字),让用户在输入前知晓要求,减少无效输入。

问题4:注册成功后直接进入应用,违背账号安全逻辑

问题描述 :初期开发中,注册成功后直接跳转到应用首页,未经过登录环节,存在账号安全漏洞,也不符合电商应用的账号使用逻辑。
解决方法 :注册成功后,仅给出成功提示,返回登录页面,让用户手动输入账号密码完成登录,确保账号使用的安全性。

问题5:注册页面无返回按钮,用户无法主动返回登录页

问题描述 :页面初期未设计返回按钮,用户进入注册页面后,只能通过注册或系统返回键退出,操作不便捷。
解决方法 :在页面左上角添加自定义返回按钮,设计与页面风格匹配的圆形按钮,点击后快速返回登录页面,提升操作便捷性。

八、页面配色方案

注册页面的配色遵循简约、统一、高辨识度的原则,采用少量配色实现页面层次区分,避免视觉杂乱,同时与电商项目的整体配色保持一致,具体配色方案如下:

配色用途 颜色值 设计说明
主题色 #FF6B00(橙色) 用于注册按钮、跳转链接、高亮文字,提升辨识度
输入框背景 #F5F5F5(浅灰色) 区分输入框与页面背景,提升输入框视觉聚焦
次要文本 #6B7280(中灰色) 用于提示文字、说明文字,不抢夺核心视觉焦点
边框/图标 #E5E7EB(浅灰色) 用于输入框边框、图标,保持页面简约风格
页面背景 #FFFFFF(白色) 基础背景色,提升页面整体明亮度

九、项目结构

为保证项目的可维护性和扩展性,本次注册功能开发严格遵循模块化、分层化的项目结构设计,将接口、页面、数据模型独立拆分,具体的项目结构如下(仅展示注册功能相关模块):

复制代码
lib/
├── api/                     // 接口封装层
│   └── register.dart        // 注册相关接口封装
├── pages/                   // 页面视图层
│   ├── register/            // 注册页面模块
│   │   └── index.dart       // 注册页面核心代码
│   └── login/               // 登录页面模块(已修改)
│       └── index.dart       // 添加注册入口,实现双向跳转
└── viewmodels/              // 数据模型层
    └── user.dart            // 用户账号相关数据模型(手机号、密码等)

结构设计优势

  1. 各层职责明确,接口、页面、模型互不耦合,便于单独维护;
  2. 新功能可直接在对应目录下添加,不影响现有功能;
  3. 团队开发时,可按模块分工,提升开发效率。

十、完整注册流程

结合前端表单验证、接口调用、页面跳转,实现从登录页面到注册成功的全流程闭环,用户操作的完整注册流程如下:

复制代码
1. 用户在登录页面点击「去注册」→ 跳转到注册页面
   ↓
2. 用户输入11位手机号 → 前端实时验证格式合法性
   ↓
3. 点击「获取验证码」→ 触发60秒倒计时,按钮置灰,调用获取验证码接口
   ↓
4. 用户输入6位数字验证码 → 验证非空
   ↓
5. 用户输入密码 → 前端验证格式(6-20位,含字母+数字)
   ↓
6. 用户输入确认密码 → 前端实时校验与密码一致性
   ↓
7. 用户勾选「用户协议和隐私政策」→ 必选操作
   ↓
8. 点击「注册」按钮 → 触发全量表单验证
   ↓
9. 验证通过 → 调用注册接口,进入加载状态
   ↓
10. 注册成功 → 弹出提示,自动返回登录页面
    ↓
11. 用户在登录页面输入新账号密码 → 完成登录

十一、与登录页面的功能对比

注册页面与登录页面同属账号体系核心页面,存在部分功能复用,但也根据各自的业务场景设计了专属功能,具体功能对比如下:

功能模块 登录页面 注册页面
手机号输入 ✅ 支持 ✅ 支持(格式验证)
验证码输入 ✅ 支持 ✅ 支持(60秒倒计时)
密码输入 ✅ 支持 ✅ 支持(格式验证+显示/隐藏)
确认密码 ❌ 不支持 ✅ 支持(一致性验证)
协议勾选 ✅ 支持 ✅ 支持(必选)
第三方登录 ✅ 支持(微信/QQ等) ❌ 不支持(新账号无第三方绑定)
账号跳转入口 「去注册」→ 跳转到注册页 「立即登录」→ 返回登录页
加载状态 ✅ 支持 ✅ 支持(防重复点击)

十二、后续优化建议

本次实现的注册页面满足电商应用的基础注册需求,结合产品迭代和用户体验提升,可在后续版本中进行以下优化,进一步提升注册转化率和用户体验:

优化项 优化说明 核心价值
密码强度指示器 实时检测密码强度,显示「弱/中/强」等级,引导用户设置安全密码 提升账号安全性,给予用户直观的密码质量反馈
手机号已注册检测 输入手机号后,实时检测是否已注册,避免用户重复注册 减少无效操作,提升注册效率
验证码自动填充 对接系统短信,实现验证码自动读取和填充,无需手动输入 提升操作便捷性,减少输入步骤
注册成功自动登录 注册成功后,自动调用登录接口,无需用户手动输入账号密码 提升用户体验,减少操作环节
运营商一键登录 集成三大运营商一键登录功能,免手机号、验证码输入 大幅提升注册转化率,实现"一键注册"
邀请码功能 添加邀请码输入框,支持电商的邀请拉新活动 适配电商运营需求,提升用户拉新效率
表单一键清空 添加表单清空按钮,方便用户重新输入 提升错误输入后的修正效率

十三、总结

本文基于Flutter 3.x技术栈,完成了电商应用注册页面的全流程开发,实现了手机号验证、密码强度校验、确认密码一致性验证、验证码倒计时、密码显示/隐藏、页面双向跳转等核心功能,同时解决了开发过程中遇到的安全隐患、用户体验等问题,形成了一套可直接复用的Flutter注册功能模块。

核心开发要点总结

  1. 密码验证通过正则表达式^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,20}$实现,兼顾安全性和易用性;
  2. 密码显示/隐藏通过obscureText属性控制,结合手势识别实现状态切换;
  3. 注册表单采用全量前置验证,验证失败给出明确提示,避免无效接口请求;
  4. 注册成功后返回登录页面,遵循"注册-登录-使用"的账号安全逻辑;
  5. 页面与接口解耦,采用模块化开发,提升项目的可维护性和扩展性;
  6. 配色和布局遵循简约原则,适配跨平台终端,提升用户体验。

✨ 坚持用 清晰的图解 +易懂的硬件架构 + 硬件解析, 让每个知识点都 简单明了 !

🚀 个人主页一只大侠的侠 · CSDN

💬 座右铭 : "所谓成功就是以自己的方式度过一生。"

相关推荐
ujainu6 小时前
让笔记触手可及:为 Flutter + OpenHarmony 鸿蒙记事本添加实时搜索(二)
笔记·flutter·openharmony
森之鸟6 小时前
鸿蒙审核常见问题避坑指南_Scroll嵌套List_Grid滑动优化
华为·harmonyos
一只大侠的侠6 小时前
Flutter开源鸿蒙跨平台训练营 Day19自定义 useFormik 实现高性能表单处理
flutter·开源·harmonyos
早點睡3906 小时前
高级进阶 React Native 鸿蒙跨平台开发:react-native-device-info 设备信息获取
react native·react.js·harmonyos
阿钱真强道6 小时前
13 JetLinks MQTT:网关设备与网关子设备 - 温控设备场景
python·网络协议·harmonyos
恋猫de小郭7 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
一只大侠的侠12 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
renke336415 小时前
Flutter for OpenHarmony:色彩捕手——基于HSL色轮与感知色差的交互式色觉训练系统
flutter
子春一17 小时前
Flutter for OpenHarmony:构建一个 Flutter 四色猜谜游戏,深入解析密码逻辑、反馈算法与经典益智游戏重构
算法·flutter·游戏