在这里插入图片描述

文本转换是日常最常用的一类“小工具”:做笔记、改文案、对齐接口字段命名的时候,经常需要临时把一段文本变成大写/小写/驼峰/去空格。

这一章我把“文本转换”做成一个独立的工具弹窗:

  • 输入即转换:不需要点按钮,输入框内容变化就实时更新输出。
  • 模式可扩展:新增模式只需要补一处枚举和一处转换分发。
  • 带统计信息:输出区域下面顺手显示字数/单词/行数,方便校对。

文本转换工具的应用

文本转换在内容编辑和数据处理中非常有用。用户可以将文本转换为大写、小写、标题格式等。在数据处理中,文本转换可以帮助统一数据格式。

本章相关代码我放在当前目录下(方便你对照阅读):

  • 入口页lib/pages/tools_page.dart
  • 弹窗组件lib/widgets/text_convert_dialog.dart
  • 转换服务lib/services/text_converter.dart

工具入口(工具列表里挂一个“文本转换”)

工具列表里我直接用 ListTile 做了一个入口,点一下弹出转换对话框:

_ToolTile(
  icon: Icons.text_fields,
  title: '文本转换',
  subtitle: '大小写、标题格式、去空格、反转、驼峰命名等',
  onTap: () => showDialog(
    context: context,
    builder: (_) => const TextConvertDialog(),
  ),
),

这一段在项目里解决什么问题?

  • 入口清晰:工具页本身只关心“有哪些工具”和“点了怎么打开”,不塞具体业务逻辑。
  • 弹窗复用TextConvertDialog 是一个独立组件,后面你想在其它页面(比如编辑器页)复用,也不用复制 UI。

文本转换对话框的实现

对话框我用 AlertDialog 包起来,内部放三块:输入、模式选择、输出。

return AlertDialog(
  title: const Text('文本转换'),
  content: SizedBox(
    width: 560,
    child: SingleChildScrollView(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [

为什么这里用了 SizedBox(width: 560)

这不是“必须的”,但在桌面端(OpenHarmony 设备或模拟器窗口较宽)时,不给宽度容易让输入框显得很窄。

这里给一个偏舒适的宽度,文本类工具用起来会顺手不少。

输入即转换:控制器 + 统一的刷新方法

我这里没有加“转换”按钮,而是把转换逻辑收敛到一个 _applyConvert() 方法里,输入变化和模式变化都走它:

final TextEditingController _inputController = TextEditingController();
final TextEditingController _outputController = TextEditingController();

void _applyConvert() {
  final input = _inputController.text;
  final output = TextConverter.convert(input, _mode);

  setState(() {
    _outputController.text = output;
    _stats = TextConverter.stats(output);
  });
}

这样写的好处

  • 更新路径单一:后面你再加“清空”“粘贴”“历史记录”等功能,只要在合适的地方调用 _applyConvert() 就行。
  • 顺便做统计:把统计信息绑定在输出结果上,不会出现“转换了但统计没变”的小毛病。

输入框:边输入边更新输出

输入框里直接监听 onChanged,把体验做成“输入即反馈”:

TextField(
  controller: _inputController,
  maxLines: 5,
  decoration: const InputDecoration(
    hintText: '输入文本',
    border: OutlineInputBorder(),
  ),
  onChanged: (_) => _applyConvert(),
),

一个实际的取舍

onChanged 会比较频繁触发,但这个转换逻辑都是纯字符串处理,量不大时问题不大。

如果你后面把它扩展成“格式化 JSON”“正则批量替换”这种更重的操作,再考虑加 debounce(比如 150ms)会更稳。

模式选择:枚举驱动下拉列表

模式不是用字符串硬编码,而是用枚举 TextConvertMode 驱动:

DropdownButtonFormField<TextConvertMode>(
  value: _mode,
  items: TextConvertMode.values
      .map(
        (m) => DropdownMenuItem(
          value: m,
          child: Text(m.label),
        ),
      )
      .toList(),
  onChanged: (value) {
    if (value == null) return;
    setState(() => _mode = value);
    _applyConvert();
  },
  decoration: const InputDecoration(
    labelText: '转换模式',
    border: OutlineInputBorder(),
  ),
),

为什么我更喜欢“枚举 + label”

  • 少写一份列表:下拉框直接从 values 来,新增/删除模式时不容易漏改 UI。
  • 类型安全convert 的参数是 TextConvertMode,不会再遇到“字符串拼错导致 default 分支吞掉”的情况。

复制:把结果塞进剪贴板

复制按钮我做成一个 FilledButton,复制成功后给一个 SnackBar 提示:

FilledButton(
  onPressed: () async {
    final messenger = ScaffoldMessenger.of(context);
    await Clipboard.setData(ClipboardData(text: _outputController.text));
    messenger.showSnackBar(const SnackBar(content: Text('已复制')));
  },
  child: const Text('复制'),
),

这里为什么要拿 messenger

因为这个按钮在 Dialog 里,直接用 ScaffoldMessenger.of(context) 能保证消息能显示出来;不然你用错 context,有时候 SnackBar 会弹不出来。

文本转换的实现

转换逻辑我放在 lib/services/text_converter.dart,弹窗不直接写字符串处理,避免后期 UI 越堆越乱。

模式定义:TextConvertMode

模式用枚举定义,UI 展示用 label,逻辑分发用枚举本身:

enum TextConvertMode {
  upper('大写'),
  lower('小写'),
  title('标题格式'),
  reverse('反转'),
  removeSpaces('去空格'),
  camel('驼峰命名');

  const TextConvertMode(this.label);
  final String label;
}

我为什么不直接用字符串列表

  • 少一份维护成本DropdownButton 直接从 values 构建,新增模式时不需要再去改 UI 列表。
  • 更不容易写错:参数是 TextConvertMode,不会出现“拼错字符串但程序还跑着”的情况。

统一分发:convert()

把所有模式都集中在一个 switch 里,弹窗只传入 textmode

static String convert(String text, TextConvertMode mode) {
  switch (mode) {
    case TextConvertMode.upper:
      return text.toUpperCase();
    case TextConvertMode.lower:
      return text.toLowerCase();
    case TextConvertMode.title:
      return _toTitleCase(text);
    case TextConvertMode.reverse:
      return text.split('').reversed.join();
    case TextConvertMode.removeSpaces:
      return text.replaceAll(RegExp(r'\s+'), '');
    case TextConvertMode.camel:
      return _toCamelCase(text);
  }
}

两个我自己用着比较爽的点

  • 去空格用 \s+:不仅去掉空格,也会把换行、tab 一起处理掉。
  • 反转直接 reversed.join():简单粗暴,做工具够用了。

标题格式:尽量不破坏原排版

标题格式这里我刻意保留了原来的空白段(比如多个空格、换行),避免转换后版面乱掉:

static String _toTitleCase(String text) {
  final parts = text.split(RegExp(r'(\s+)'));
  return parts.map((p) {
    if (p.trim().isEmpty) return p;
    final first = p.substring(0, 1).toUpperCase();
    final rest = p.length > 1 ? p.substring(1).toLowerCase() : '';
    return '$first$rest';
  }).join();
}

这段写法比较“笨”,但更像工具该有的行为

只改大小写,不动用户原来的格式。

驼峰命名:给字段改名用

我把分隔符按 空格 / 下划线 / 中横线 统一处理,平时改接口字段名基本就覆盖到了:

final words = text
    .trim()
    .split(RegExp(r'[\s_-]+'))
    .where((w) => w.isNotEmpty)
    .toList();

这段为什么不直接 split(' ')

因为真实场景里经常是 user_name / user-name / user name 混着来,用一个正则统一切开更省事。

文本统计:输出区下面那一行数字

统计我只做三项:字数、单词数、行数,够用就行:

static TextStats stats(String text) {
  final chars = text.runes.length;
  final words = text
      .trim()
      .split(RegExp(r'\s+'))
      .where((w) => w.isNotEmpty)
      .length;

  final lines = text.isEmpty ? 0 : text.split('\n').length;
  return (chars: chars, words: words, lines: lines);
}

一个小细节

chars 用了 runes.length,比 text.length 更贴近“人看到的字符数”。如果你想抠得更细(比如 emoji),可以再单独升级这块。

更多转换模式的支持

后面你要扩展模式,路径很固定:

  • 加枚举:在 TextConvertMode 里加一项。
  • 加分支:在 TextConverter.convert() 里补一个 case

下拉框来自 TextConvertMode.values,所以 UI 基本不用改。

总结

这一章做完之后,工具页上“文本转换”就能当成一个标准的小模块复用:UI 负责交互,TextConverter 负责纯逻辑。后面你继续做其它“工具类功能”(比如正则、JSON 格式化),照这个拆分方式会轻松很多。


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

Logo

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

更多推荐