Flutter开源鸿蒙跨平台训练营 Day12从零开发通用型登录页面

Flutter电商实战:从零开发通用型登录页面

在电商类移动应用中,登录页面是用户触达应用核心功能的关键入口,其交互体验、功能完整性直接影响用户的初始使用感受与留存率。本文基于Flutter 3.x技术栈,结合跨平台开发实战经验,从零实现一款适配电商场景的通用型登录页面,涵盖手机号验证、60秒验证码倒计时、防重复登录、协议勾选、第三方登录等核心功能,同时解决开发过程中常见的倒计时异常、输入框遮挡、按钮重复点击等问题,完成从数据模型设计到个人中心集成的全流程开发,实现登录功能的高可用与高体验。

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


一、项目基础说明

1.1 技术栈与项目结构

  • Flutter版本:3.x
  • 项目整体架构:底部导航栏+多页面架构(首页、分类、购物车、个人中心)
  • 核心依赖:Dio(网络请求)、原生状态管理(StatefulWidget)
  • 开发目标:实现高内聚、低耦合的电商用户登录功能,支持后续跨平台适配与功能扩展

1.2 核心功能需求

本次开发的登录页面需满足电商场景的基础登录诉求,所有功能模块均做合理性校验与交互优化,具体功能要求如下:

功能模块 核心要求
手机号输入 仅支持11位数字输入,做格式合法性校验,非11位手机号禁止获取验证码
验证码输入 6位纯数字输入,与手机号绑定完成登录校验
验证码倒计时 点击获取后启动60秒倒计时,期间按钮置灰不可点击,防止重复获取
登录按钮 防重复点击,点击后显示加载状态;协议未勾选/信息未填时按钮置灰
协议勾选 必选项,未勾选时无法触发登录操作,适配合规性要求
第三方登录 提供微信、QQ第三方快捷登录入口,适配电商用户多样化登录习惯

1.3 整体开发流程

遵循需求分析→数据模型设计→API接口封装→UI页面实现→状态管理→业务集成→问题调试的开发流程,保证代码的规范性与开发的逻辑性,具体流程如下:

复制代码
需求分析 → 数据模型设计 → API接口封装 → 登录页面UI开发 → 个人中心业务集成 → 状态管理与交互优化 → 问题排查与测试

二、数据模型与API接口封装

2.1 用户信息数据模型

针对电商场景的用户基础信息需求,设计轻量型UserInfo数据模型,包含用户ID、手机号、昵称、头像等核心字段,支持JSON与模型的相互转换,便于登录后数据的存储与传递。

文件路径:lib/viewmodels/user.dart

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,
  });

  // 从JSON解析为UserInfo模型
  factory UserInfo.fromJSON(Map<String, dynamic> json) {
    return UserInfo(
      id: json['id'] ?? '',
      nickname: json['nickname'],
      avatar: json['avatar'],
      phone: json['phone'] ?? '',
    );
  }

  // 将UserInfo模型转换为JSON
  Map<String, dynamic> toJSON() {
    return {
      'id': id,
      'nickname': nickname,
      'avatar': avatar,
      'phone': phone,
    };
  }
}

2.2 登录相关API接口封装

基于Dio实现网络请求的封装,抽离登录场景的核心接口:获取验证码、手机号+验证码登录、退出登录,为便于本地测试,所有接口均添加模拟延迟,实际项目中可直接替换为真实后端接口地址与参数。

文件路径:lib/api/login.dart

dart 复制代码
import 'package:dio/dio.dart';
import 'package:your_project_name/utils/DioRequest.dart'; // 自定义Dio配置
import 'package:your_project_name/viewmodels/user.dart'; // 导入用户信息模型

/// 获取短信验证码接口
/// [phone]:用户输入的11位手机号
Future<void> getVerifyCodeAPI(String phone) async {
  // 实际项目替换为真实接口
  // await dioRequest.post('/auth/send-sms-code', data: {'phone': phone});
  // 模拟网络请求延迟500ms
  await Future.delayed(const Duration(milliseconds: 500));
}

/// 手机号+验证码登录接口
/// [phone]:用户手机号
/// [code]:6位验证码
/// 返回值:UserInfo用户信息模型
Future<UserInfo> loginAPI(String phone, String code) async {
  // 实际项目替换为真实接口
  // final result = await dioRequest.post('/auth/phone-login', data: {'phone': phone, 'code': code});
  // return UserInfo.fromJSON(result.data);
  // 模拟网络请求延迟1s
  await Future.delayed(const Duration(seconds: 1));
  // 返回模拟用户数据,实际项目删除此部分
  return UserInfo(
    id: '100001',
    nickname: 'Flutter电商用户',
    avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200',
    phone: phone,
  );
}

/// 退出登录接口
Future<void> logoutAPI() async {
  // 实际项目替换为真实接口
  // await dioRequest.post('/auth/logout');
  // 模拟网络请求延迟500ms
  await Future.delayed(const Duration(milliseconds: 500));
}

三、登录页面UI完整实现

登录页面采用Scaffold+SafeArea+SingleChildScrollView 的布局结构,适配不同屏幕尺寸,同时解决键盘弹出时的页面遮挡问题;整体配色以橙色(主色值0xFFFF6B00)为主色调,契合电商应用的视觉风格,文件路径为lib/pages/login/index.dart

3.1 页面状态管理初始化

使用Flutter原生StatefulWidget实现页面状态管理,初始化输入框控制器、状态标识、倒计时变量等核心数据,统一管理页面所有交互状态。

dart 复制代码
import 'package:flutter/material.dart';
import 'package:your_project_name/api/login.dart';
import 'package:your_project_name/viewmodels/user.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  // 输入框控制器
  final TextEditingController _phoneController = TextEditingController();
  final TextEditingController _codeController = TextEditingController();

  // 状态标识
  bool _agreeToTerms = false;    // 是否同意用户协议
  bool _isLoggingIn = false;     // 是否正在登录(防止重复点击)
  int _countdown = 0;            // 验证码倒计时秒数
  late Timer _countdownTimer;    // 倒计时定时器

  // 页面主色调
  final Color _primaryColor = const Color(0xFFFF6B00);

  // 页面销毁时释放资源
  @override
  void dispose() {
    _phoneController.dispose();
    _codeController.dispose();
    if (_countdown > 0) {
      _countdownTimer.cancel(); // 销毁定时器,防止内存泄漏
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildLogoAndTitle(), // Logo与标题区域
              const SizedBox(height: 48),
              _buildPhoneInput(),   // 手机号输入框
              const SizedBox(height: 16),
              _buildCodeInput(),    // 验证码输入框+倒计时按钮
              const SizedBox(height: 24),
              _buildLoginButton(),  // 登录按钮
              const SizedBox(height: 20),
              _buildTermsCheck(),   // 协议勾选框
              const SizedBox(height: 40),
              _buildThirdPartyLogin(), // 第三方登录区域
            ],
          ),
        ),
      ),
    );
  }
}

3.2 核心UI组件实现

(1)Logo与标题区域

简洁的视觉入口,包含应用Logo与登录标题,提升页面辨识度。

dart 复制代码
// Logo与标题
Widget _buildLogoAndTitle() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Container(
        width: 60,
        height: 60,
        decoration: BoxDecoration(
          color: _primaryColor.withOpacity(0.1),
          borderRadius: BorderRadius.circular(12),
          image: const DecorationImage(
            image: NetworkImage('https://img-blog.csdnimg.cn/20240105112827959.png'),
            fit: BoxFit.contain,
          ),
        ),
      ),
      const SizedBox(height: 16),
      const Text(
        '欢迎登录',
        style: TextStyle(
          fontSize: 28,
          fontWeight: FontWeight.bold,
          color: Colors.black87,
        ),
      ),
      const Text(
        '输入手机号即可快速登录',
        style: TextStyle(
          fontSize: 14,
          color: Colors.grey,
        ),
      ),
    ],
  );
}
(2)手机号输入框

限制11位数字输入,隐藏默认字符计数器,添加电话图标前缀,优化输入体验;背景采用浅灰色,提升视觉层次感。

dart 复制代码
// 手机号输入框
Widget _buildPhoneInput() {
  return Container(
    decoration: BoxDecoration(
      color: Colors.grey[100],
      borderRadius: BorderRadius.circular(12),
      boxShadow: [
        BoxShadow(
          color: Colors.grey.withOpacity(0.1),
          blurRadius: 4,
          offset: const Offset(0, 2),
        )
      ],
    ),
    child: TextField(
      controller: _phoneController,
      keyboardType: TextInputType.phone,
      maxLength: 11,
      style: const TextStyle(fontSize: 16),
      decoration: const InputDecoration(
        hintText: '请输入11位手机号',
        hintStyle: TextStyle(color: Colors.grey, fontSize: 16),
        prefixIcon: Icon(Icons.phone_outlined, color: Colors.grey),
        counterText: '', // 隐藏字符计数
        border: InputBorder.none,
        contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 18),
      ),
      onChanged: (value) {
        setState(() {}); // 输入变化刷新页面,适配按钮状态
      },
    ),
  );
}
(3)验证码输入框+倒计时按钮

采用行布局,左侧为6位数字验证码输入框,右侧为倒计时按钮;按钮根据倒计时状态自动置灰/激活,点击后触发验证码获取与倒计时逻辑。

dart 复制代码
// 验证码输入框+倒计时按钮
Widget _buildCodeInput() {
  bool isPhoneValid = _isValidPhone(_phoneController.text);
  return Row(
    children: [
      Expanded(
        child: Container(
          decoration: BoxDecoration(
            color: Colors.grey[100],
            borderRadius: BorderRadius.circular(12),
            boxShadow: [
              BoxShadow(
                color: Colors.grey.withOpacity(0.1),
                blurRadius: 4,
                offset: const Offset(0, 2),
              )
            ],
          ),
          child: TextField(
            controller: _codeController,
            keyboardType: TextInputType.number,
            maxLength: 6,
            style: const TextStyle(fontSize: 16),
            enabled: isPhoneValid, // 手机号不合法时输入框置灰
            decoration: const InputDecoration(
              hintText: '请输入6位验证码',
              hintStyle: TextStyle(color: Colors.grey, fontSize: 16),
              prefixIcon: Icon(Icons.lock_outline, color: Colors.grey),
              counterText: '',
              border: InputBorder.none,
              contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 18),
            ),
            onChanged: (value) {
              setState(() {});
            },
          ),
        ),
      ),
      const SizedBox(width: 12),
      SizedBox(
        width: 120,
        height: 56,
        child: ElevatedButton(
          onPressed: (_countdown > 0 || !isPhoneValid) ? null : _getVerifyCode,
          style: ElevatedButton.styleFrom(
            backgroundColor: _primaryColor,
            disabledBackgroundColor: Colors.grey[300],
            foregroundColor: Colors.white,
            disabledForegroundColor: Colors.white.withOpacity(0.6),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(12),
            ),
            elevation: 0,
            padding: EdgeInsets.zero,
          ),
          child: Text(
            _countdown > 0 ? '${_countdown}s' : '获取验证码',
            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
          ),
        ),
      ),
    ],
  );
}
(4)登录按钮

实现防重复点击与加载状态,当手机号不合法/验证码未填/协议未勾选/正在登录时,按钮自动置灰;点击后触发登录逻辑,显示加载动画。

dart 复制代码
// 登录按钮
Widget _buildLoginButton() {
  bool canLogin = _isValidPhone(_phoneController.text) &&
      _codeController.text.length == 6 &&
      _agreeToTerms &&
      !_isLoggingIn;
  return SizedBox(
    width: double.infinity,
    height: 56,
    child: ElevatedButton(
      onPressed: canLogin ? _handleLogin : null,
      style: ElevatedButton.styleFrom(
        backgroundColor: _primaryColor,
        disabledBackgroundColor: Colors.grey[300],
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
        elevation: 0,
        padding: EdgeInsets.zero,
      ),
      child: _isLoggingIn
          ? const CircularProgressIndicator(
              color: Colors.white,
              strokeWidth: 2,
            )
          : const Text(
              '立即登录',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
            ),
    ),
  );
}
(5)协议勾选与第三方登录

协议勾选为必选组件,关联登录按钮状态;第三方登录提供微信、QQ图标入口,适配电商用户快捷登录需求,样式简洁统一。

dart 复制代码
// 协议勾选
Widget _buildTermsCheck() {
  return Row(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
      Checkbox(
        value: _agreeToTerms,
        onChanged: (value) {
          setState(() {
            _agreeToTerms = value ?? false;
          });
        },
        activeColor: _primaryColor,
        materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
      ),
      Expanded(
        child: RichText(
          text: TextSpan(
            style: const TextStyle(fontSize: 12, color: Colors.grey),
            children: [
              const TextSpan(text: '我已阅读并同意'),
              TextSpan(
                text: '《用户服务协议》',
                style: TextStyle(color: _primaryColor),
              ),
              const TextSpan(text: '和'),
              TextSpan(
                text: '《隐私政策》',
                style: TextStyle(color: _primaryColor),
              ),
            ],
          ),
        ),
      ),
    ],
  );
}

// 第三方登录
Widget _buildThirdPartyLogin() {
  return Column(
    children: [
      const Text(
        '其他登录方式',
        style: TextStyle(fontSize: 12, color: Colors.grey),
      ),
      const SizedBox(height: 16),
      Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 微信登录
          _buildThirdPartyItem(Icons.wechat, Colors.green),
          const SizedBox(width: 40),
          // QQ登录
          _buildThirdPartyItem(Icons.question_answer, Colors.blue),
        ],
      ),
    ],
  );
}

// 第三方登录子项
Widget _buildThirdPartyItem(IconData icon, Color color) {
  return GestureDetector(
    onTap: () {
      // 第三方登录逻辑,实际项目自行实现
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('${icon.name}登录功能开发中')),
      );
    },
    child: Container(
      width: 48,
      height: 48,
      decoration: BoxDecoration(
        color: color.withOpacity(0.1),
        borderRadius: BorderRadius.circular(24),
      ),
      child: Icon(icon, color: color, size: 24),
    ),
  );
}

3.3 核心交互逻辑实现

(1)手机号合法性校验

封装手机号校验方法,仅允许11位纯数字手机号通过校验,作为获取验证码与登录的前置条件。

dart 复制代码
// 手机号合法性校验
bool _isValidPhone(String phone) {
  if (phone.length != 11) return false;
  // 正则匹配11位手机号(以13/14/15/16/17/18/19开头)
  final RegExp phoneReg = RegExp(r'^1[3-9]\d{9}$');
  return phoneReg.hasMatch(phone);
}
(2)验证码倒计时逻辑

点击获取验证码后,启动60秒倒计时,通过Timer实现秒数递减,页面实时刷新;倒计时结束后自动重置按钮状态,同时做定时器判空,防止页面销毁后定时器继续运行。

dart 复制代码
// 获取验证码并启动倒计时
Future<void> _getVerifyCode() async {
  String phone = _phoneController.text;
  try {
    // 调用获取验证码API
    await getVerifyCodeAPI(phone);
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('验证码发送成功,请注意查收')),
      duration: const Duration(seconds: 2),
    );
    // 启动60秒倒计时
    setState(() {
      _countdown = 60;
    });
    _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        _countdown--;
        if (_countdown <= 0) {
          timer.cancel(); // 倒计时结束销毁定时器
        }
      });
    });
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('验证码发送失败:$e')),
      backgroundColor: Colors.red,
    );
  }
}
(3)登录核心逻辑

点击登录后,先做本地参数校验,再调用登录API;请求过程中置为"正在登录"状态,防止重复点击;请求成功后返回用户信息并跳转至个人中心,失败则提示错误信息。

dart 复制代码
// 处理登录逻辑
Future<void> _handleLogin() async {
  String phone = _phoneController.text;
  String code = _codeController.text;
  try {
    // 设置登录状态,防止重复点击
    setState(() {
      _isLoggingIn = true;
    });
    // 调用登录API
    UserInfo userInfo = await loginAPI(phone, code);
    // 登录成功,返回上一页并传递用户信息
    if (mounted) {
      Navigator.pop(context, userInfo);
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('登录成功')),
      );
    }
  } catch (e) {
    // 登录失败,提示错误
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('登录失败:$e'), backgroundColor: Colors.red),
      );
    }
  } finally {
    // 无论成功失败,重置登录状态
    if (mounted) {
      setState(() {
        _isLoggingIn = false;
      });
    }
  }
}

四、个人中心业务集成

登录功能最终需与个人中心联动,实现未登录状态显示登录入口→点击跳转登录页→登录成功后返回个人中心并刷新用户信息的完整业务流程,核心实现逻辑如下:

4.1 未登录状态展示

在个人中心页面判断用户登录状态,若未登录则显示登录/注册入口,点击后跳转至登录页面。

dart 复制代码
// 个人中心未登录状态
Widget _buildUnLoginWidget() {
  return GestureDetector(
    onTap: () async {
      // 跳转登录页,并接收返回的用户信息
      UserInfo? userInfo = await Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => const LoginPage()),
      );
      // 登录成功,刷新用户信息
      if (userInfo != null) {
        setState(() {
          _currentUser = userInfo; // _currentUser为个人中心的用户信息变量
        });
      }
    },
    child: Container(
      alignment: Alignment.center,
      padding: const EdgeInsets.symmetric(vertical: 40),
      child: const Column(
        children: [
          Icon(Icons.person_outline, size: 60, color: Colors.grey),
          SizedBox(height: 16),
          Text(
            '请登录账号',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
          ),
          SizedBox(height: 8),
          Text(
            '登录后可查看我的订单、收藏等功能',
            style: TextStyle(fontSize: 14, color: Colors.grey),
          ),
        ],
      ),
    ),
  );
}

4.2 登录成功后数据处理

登录页面通过Navigator.pop(context, userInfo)将用户信息回传至个人中心,个人中心接收后更新本地状态,刷新页面展示用户头像、昵称、手机号等信息,完成登录状态的同步。

五、开发常见问题及解决方案

在登录页面开发过程中,会遇到多个交互与性能问题,以下为电商场景中最常见的5个问题及针对性解决方案,保证页面的稳定性与体验性。

问题1:验证码倒计时重复触发

问题原因 :快速多次点击"获取验证码"按钮,导致多个定时器同时运行,倒计时混乱。
解决方案 :按钮点击条件添加_countdown > 0判断,倒计时期间按钮置灰不可点击;同时在获取验证码方法中做前置校验,确保同一时间仅能启动一个定时器。

问题2:页面销毁后倒计时继续运行

问题原因 :定时器未及时销毁,导致页面销毁后仍在执行,造成内存泄漏。
解决方案 :在dispose生命周期中判空并销毁定时器;同时在定时器回调与网络请求中添加mounted判断,确保页面未销毁时再执行状态更新。

问题3:登录按钮重复点击

问题原因 :网络请求存在延迟,用户多次点击按钮导致多次调用登录API。
解决方案 :添加_isLoggingIn状态标识,点击后立即置为true,按钮置灰;请求结束(成功/失败)后再置为false,恢复按钮状态。

问题4:TextField字符计数器遮挡

问题原因 :Flutter默认的字符计数器会占用输入框底部空间,导致布局拥挤,影响视觉体验。
解决方案 :设置counterText: '',隐藏默认字符计数器;通过maxLength限制输入长度,结合输入框提示文字引导用户输入正确位数。

问题5:键盘弹出遮挡输入框

问题原因 :键盘弹出后,页面高度不足,输入框被键盘遮挡,无法看到输入内容。
解决方案 :使用SingleChildScrollView包裹页面所有内容,键盘弹出时页面可滚动;同时使用SafeArea避开系统状态栏与底部导航栏,保证布局适配。

六、项目优化建议

6.1 功能优化

  1. 添加密码登录方式,适配不同用户登录习惯;
  2. 增加验证码过期提醒重新获取功能,提升容错性;
  3. 实现记住手机号功能,基于SharedPreferences存储,下次登录自动填充;
  4. 添加图形验证码校验,防止短信验证码被恶意刷取。

6.2 性能与体验优化

  1. 替换原生StatefulWidget为Provider/BLoC状态管理,便于跨页面共享登录状态;
  2. 对输入框做防抖处理,减少不必要的页面刷新;
  3. 添加输入框焦点管理,手机号输入完成后自动聚焦至验证码输入框;
  4. 优化网络请求,添加请求拦截器错误统一处理,提升代码可维护性。

6.3 合规性优化

  1. 协议勾选添加单独点击跳转功能,适配合规性要求;
  2. 增加隐私政策弹窗,首次打开应用时强制展示;
  3. 对用户手机号做脱敏处理(如138****1234),保护用户隐私。

七、总结

本文基于Flutter 3.x实现了电商场景下的通用型登录页面,完成了数据模型设计、API接口封装、UI页面开发、状态管理、业务集成的全流程开发,同时解决了倒计时异常、按钮重复点击、输入框遮挡等开发常见问题。

本登录页面具备高可用、高体验、高可扩展的特点,所有功能均做了合理性校验与交互优化,可直接适配电商、社交、工具类等各类Flutter跨平台应用;同时代码结构清晰,模块划分明确,便于后续的功能扩展与跨平台适配(如鸿蒙、iOS、Android)。

在实际项目开发中,可根据业务需求在此基础上做个性化定制,重点关注状态管理的合理性、网络请求的稳定性、用户体验的流畅性合规性要求,打造更符合产品定位的登录页面。

八、参考资料

  1. Flutter官方文档:https://flutter.dev/docs
  2. Dio网络请求官方文档:https://pub.dev/packages/dio
  3. Flutter跨平台开发实战:鸿蒙与Flutter混合开发指南
  4. 电商APP登录页面设计规范与交互体验白皮书

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

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

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

相关推荐
wenzhangli76 小时前
OoderAgent 企业版 2.0 发布的意义:一次生态战略的全面升级
人工智能·开源
rainbow68896 小时前
C++开源库dxflib解析DXF文件实战
开发语言·c++·开源
猫头虎6 小时前
基于信创openEuler系统安装部署OpenTeleDB开源数据库的实战教程
数据库·redis·sql·mysql·开源·nosql·database
晚霞的不甘6 小时前
Flutter for OpenHarmony天气卡片应用:用枚举与动画打造沉浸式多城市天气浏览体验
前端·flutter·云原生·前端框架
零一iTEM6 小时前
MAX98357A_音频输出测试
单片机·嵌入式硬件·开源·音视频·硬件工程
子春一6 小时前
Flutter for OpenHarmony:语桥 - 基于Flutter的离线多语言短语速查工具实现与国际化设计理念
flutter
前端不太难7 小时前
HarmonyOS App 工程深水区:从能跑到可控
华为·状态模式·harmonyos
猫头虎7 小时前
如何使用Docker部署OpenClaw汉化中文版?
运维·人工智能·docker·容器·langchain·开源·aigc
万少7 小时前
端云一体 一天开发的元服务-奇趣故事匣经验分享
前端·ai编程·harmonyos