短信二维码可以将短信信息编码成二维码,扫描后可以直接打开短信应用并预填收件人和内容。这在营销活动、投票、订阅服务等场景下很有用。这篇文章介绍短信二维码生成页面的实现,包括 sms 协议、内容编码、字数限制等功能。

sms 协议介绍

短信二维码使用 sms 协议,格式如下:

sms:电话号码?body=短信内容

例如:sms:13800138000?body=Hello%20World

各部分说明:

sms::协议前缀,表示这是一个短信链接

电话号码:收件人的手机号码

body:短信内容,需要进行 URL 编码

扫描后,手机会打开短信应用,自动填入收件人和内容,用户只需点击发送即可。

SmsQrView 的基础结构

先来看文件的导入和类定义:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../data/models/qr_record.dart';
import 'generate_controller.dart';

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

  
  State<SmsQrView> createState() => _SmsQrViewState();
}

导入了必要的包。SmsQrView 使用 StatefulWidget,因为需要管理多个输入框的状态。

状态类的定义

状态类中定义了两个输入控制器:

class _SmsQrViewState extends State<SmsQrView> {
  final _phoneController = TextEditingController();
  final _messageController = TextEditingController();
  final _controller = Get.find<GenerateController>();

  
  void dispose() {
    _phoneController.dispose();
    _messageController.dispose();
    super.dispose();
  }

_phoneController 控制收件人号码输入框,_messageController 控制短信内容输入框。dispose 中释放两个控制器。

Scaffold 和 AppBar

build 方法返回页面结构:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('短信二维码')),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [

Scaffold 提供基础页面结构,AppBar 标题为"短信二维码"。body 使用 SingleChildScrollView 包裹,支持内容滚动。

收件人号码输入

第一个输入项是收件人号码:

            Text('收件人号码', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
            SizedBox(height: 8.h),
            TextField(
              controller: _phoneController,
              keyboardType: TextInputType.phone,
              decoration: InputDecoration(
                hintText: '输入手机号码',
                prefixIcon: const Icon(Icons.phone),
                border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
              ),
            ),
            SizedBox(height: 16.h),

keyboardType 设为 TextInputType.phone,显示数字键盘。prefixIcon 使用电话图标。

短信内容输入

第二个输入项是短信内容:

            Text('短信内容', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
            SizedBox(height: 8.h),
            TextField(
              controller: _messageController,
              maxLines: 4,
              maxLength: 160,
              decoration: InputDecoration(
                hintText: '输入短信内容',
                border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
              ),
            ),
          ],
        ),
      ),

maxLines 设为 4,让输入框显示 4 行高度。maxLength 设为 160,这是单条短信的标准长度限制。

TextField 会自动显示字数计数器,用户可以看到还能输入多少字符。

底部生成按钮

页面底部是生成按钮:

      bottomNavigationBar: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(16.w),
          child: ElevatedButton(
            onPressed: () => _controller.generateQr(
              'sms:${_phoneController.text}?body=${Uri.encodeComponent(_messageController.text)}',
              QrType.sms,
            ),
            style: ElevatedButton.styleFrom(
              minimumSize: Size(double.infinity, 48.h),
              backgroundColor: Theme.of(context).primaryColor,
              foregroundColor: Colors.white,
            ),
            child: const Text('生成二维码'),
          ),
        ),
      ),
    );
  }
}

点击按钮时,将号码和内容拼接成 sms 协议格式。Uri.encodeComponent 对短信内容进行 URL 编码,确保特殊字符能正确传递。

输入验证

在生成前验证输入:

void _handleGenerate() {
  final phone = _phoneController.text.trim();
  final message = _messageController.text;
  
  if (phone.isEmpty) {
    Get.snackbar('提示', '请输入收件人号码', snackPosition: SnackPosition.BOTTOM);
    return;
  }
  
  // 验证手机号格式
  if (!_isValidPhone(phone)) {
    Get.snackbar('提示', '请输入有效的手机号码', snackPosition: SnackPosition.BOTTOM);
    return;
  }
  
  if (message.isEmpty) {
    Get.snackbar('提示', '请输入短信内容', snackPosition: SnackPosition.BOTTOM);
    return;
  }
  
  final sms = 'sms:$phone?body=${Uri.encodeComponent(message)}';
  _controller.generateQr(sms, QrType.sms);
}

bool _isValidPhone(String phone) {
  final digits = phone.replaceAll(RegExp(r'[^\d]'), '');
  return digits.length == 11 && digits.startsWith('1');
}

验证号码不为空且是有效的手机号,短信内容不为空。

字数统计

自定义字数统计显示:

Widget _buildMessageField() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text('短信内容', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
          Text(
            '${_messageController.text.length}/160',
            style: TextStyle(
              fontSize: 12.sp,
              color: _messageController.text.length > 140 ? Colors.orange : Colors.grey,
            ),
          ),
        ],
      ),
      SizedBox(height: 8.h),
      TextField(
        controller: _messageController,
        maxLines: 4,
        maxLength: 160,
        decoration: InputDecoration(
          hintText: '输入短信内容',
          border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
          counterText: '', // 隐藏默认计数器
        ),
        onChanged: (_) => setState(() {}),
      ),
      SizedBox(height: 4.h),
      Text(
        '超过70个汉字将分成多条短信发送',
        style: TextStyle(fontSize: 11.sp, color: Colors.grey),
      ),
    ],
  );
}

字数统计显示在标题行,接近上限时变成橙色。底部提示用户关于短信分条的规则。

短信模板

提供一些常用的短信模板:

Widget _buildTemplates() {
  final templates = [
    {'name': '验证码', 'content': '您的验证码是:____,5分钟内有效。'},
    {'name': '会议通知', 'content': '会议通知:____,时间:____,地点:____'},
    {'name': '订单确认', 'content': '您的订单已确认,订单号:____'},
    {'name': '活动邀请', 'content': '诚邀您参加____活动,时间:____'},
    {'name': '投票', 'content': '回复数字参与投票:1.____  2.____'},
  ];
  
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      SizedBox(height: 16.h),
      Text('快速模板', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
      SizedBox(height: 8.h),
      Wrap(
        spacing: 8.w,
        runSpacing: 8.h,
        children: templates.map((template) => ActionChip(
          label: Text(template['name']!),
          onPressed: () => _messageController.text = template['content']!,
        )).toList(),
      ),
    ],
  );
}

点击模板按钮可以快速填充短信内容,用户只需修改占位符即可。

从通讯录选择

提供从通讯录选择联系人的功能:

import 'package:contacts_service/contacts_service.dart';

Future<void> _pickFromContacts() async {
  final contact = await ContactsService.openDeviceContactPicker();
  
  if (contact != null && contact.phones != null && contact.phones!.isNotEmpty) {
    final phone = contact.phones!.first.value;
    if (phone != null) {
      final cleanPhone = phone.replaceAll(RegExp(r'[^\d]'), '');
      _phoneController.text = cleanPhone;
      
      Get.snackbar(
        '已选择',
        contact.displayName ?? cleanPhone,
        snackPosition: SnackPosition.BOTTOM,
      );
    }
  }
}

使用 contacts_service 插件打开系统通讯录选择器。

短信预览

显示短信预览:

Widget _buildPreview() {
  if (_phoneController.text.isEmpty && _messageController.text.isEmpty) {
    return const SizedBox.shrink();
  }
  
  return Card(
    margin: EdgeInsets.only(top: 16.h),
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              const Icon(Icons.sms, color: Colors.green),
              SizedBox(width: 8.w),
              Text('短信预览', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600)),
            ],
          ),
          Divider(height: 24.h),
          Row(
            children: [
              Text('收件人: ', style: TextStyle(fontSize: 12.sp, color: Colors.grey)),
              Text(
                _phoneController.text.isEmpty ? '未填写' : _phoneController.text,
                style: TextStyle(fontSize: 13.sp),
              ),
            ],
          ),
          SizedBox(height: 8.h),
          Container(
            width: double.infinity,
            padding: EdgeInsets.all(12.w),
            decoration: BoxDecoration(
              color: Colors.green[50],
              borderRadius: BorderRadius.circular(8.r),
            ),
            child: Text(
              _messageController.text.isEmpty ? '短信内容' : _messageController.text,
              style: TextStyle(fontSize: 13.sp),
            ),
          ),
        ],
      ),
    ),
  );
}

预览区域模拟短信气泡样式,让用户看到最终效果。

测试发送

提供测试发送功能:

import 'package:url_launcher/url_launcher.dart';

Future<void> _testSend() async {
  final phone = _phoneController.text.trim();
  final message = _messageController.text;
  
  if (phone.isEmpty) {
    Get.snackbar('提示', '请先输入收件人号码', snackPosition: SnackPosition.BOTTOM);
    return;
  }
  
  final sms = 'sms:$phone?body=${Uri.encodeComponent(message)}';
  final uri = Uri.parse(sms);
  
  if (await canLaunchUrl(uri)) {
    await launchUrl(uri);
  } else {
    Get.snackbar('错误', '无法打开短信应用', snackPosition: SnackPosition.BOTTOM);
  }
}

使用 url_launcher 插件打开 sms 链接,会启动系统短信应用。

多收件人支持

支持添加多个收件人:

class _SmsQrViewState extends State<SmsQrView> {
  final List<TextEditingController> _phoneControllers = [TextEditingController()];
  
  void _addRecipient() {
    setState(() {
      _phoneControllers.add(TextEditingController());
    });
  }
  
  void _removeRecipient(int index) {
    if (_phoneControllers.length > 1) {
      setState(() {
        _phoneControllers[index].dispose();
        _phoneControllers.removeAt(index);
      });
    }
  }
  
  String _generateSms() {
    final phones = _phoneControllers
        .map((c) => c.text.trim())
        .where((p) => p.isNotEmpty)
        .join(',');
    
    return 'sms:$phones?body=${Uri.encodeComponent(_messageController.text)}';
  }
}

多个收件人用逗号分隔。

特殊字符处理

处理短信内容中的特殊字符:

String _encodeMessage(String message) {
  // URL 编码
  var encoded = Uri.encodeComponent(message);
  
  // 某些设备需要特殊处理换行符
  encoded = encoded.replaceAll('%0A', '%0D%0A');
  
  return encoded;
}

不同设备对换行符的处理可能不同,需要特殊处理。

短信费用提示

显示短信费用提示:

Widget _buildCostHint() {
  final length = _messageController.text.length;
  int smsCount;
  
  // 计算短信条数
  if (length <= 70) {
    smsCount = 1;
  } else {
    smsCount = (length / 67).ceil(); // 长短信每条67字
  }
  
  return Padding(
    padding: EdgeInsets.only(top: 8.h),
    child: Row(
      children: [
        Icon(Icons.info_outline, size: 16.sp, color: Colors.grey),
        SizedBox(width: 4.w),
        Text(
          '预计发送 $smsCount 条短信',
          style: TextStyle(fontSize: 12.sp, color: Colors.grey),
        ),
      ],
    ),
  );
}

根据内容长度计算短信条数,提醒用户费用。

表情符号支持

提供常用表情符号:

Widget _buildEmojis() {
  final emojis = ['😊', '👍', '❤️', '🎉', '✅', '⭐', '📱', '💬'];
  
  return Wrap(
    spacing: 8.w,
    children: emojis.map((emoji) => GestureDetector(
      onTap: () {
        final text = _messageController.text;
        final selection = _messageController.selection;
        final newText = text.replaceRange(
          selection.start,
          selection.end,
          emoji,
        );
        _messageController.text = newText;
        _messageController.selection = TextSelection.collapsed(
          offset: selection.start + emoji.length,
        );
      },
      child: Container(
        padding: EdgeInsets.all(8.w),
        child: Text(emoji, style: TextStyle(fontSize: 20.sp)),
      ),
    )).toList(),
  );
}

点击表情符号可以插入到短信内容中。

历史记录

显示最近发送的短信:

Widget _buildRecentSms() {
  return Obx(() {
    final recentSms = _controller.recentRecords
        .where((r) => r.type == QrType.sms)
        .take(3)
        .toList();
    
    if (recentSms.isEmpty) return const SizedBox.shrink();
    
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SizedBox(height: 16.h),
        Text('最近使用', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
        SizedBox(height: 8.h),
        ...recentSms.map((record) {
          final parts = record.content.replaceFirst('sms:', '').split('?body=');
          final phone = parts[0];
          final body = parts.length > 1 ? Uri.decodeComponent(parts[1]) : '';
          
          return ListTile(
            dense: true,
            leading: const Icon(Icons.history),
            title: Text(phone),
            subtitle: Text(
              body,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
            onTap: () {
              _phoneController.text = phone;
              _messageController.text = body;
            },
          );
        }),
      ],
    );
  });
}

从历史记录中提取号码和内容,点击可以快速填充。

小结

短信二维码生成页面使用 sms 协议编码短信信息。页面包含收件人号码和短信内容两个输入字段,支持多收件人、模板、表情符号等功能。

URL 编码确保特殊字符能正确传递。字数统计和费用提示帮助用户了解短信长度限制。预览和测试发送功能让用户在生成二维码前确认内容。

短信二维码在营销活动、投票、订阅服务等场景下很有用,扫描后可以直接打开短信应用发送预设内容。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐