Flutter for OpenHarmony Web开发助手App实战:文本统计

做 Web 开发助手的时候,"文本统计"是很常见的一个小工具:复制一段日志、粘贴一段文案,就能马上看到字符数、行数、段落数,顺手还能做一些清洗。

这一节我把实现拆成UI + 统计逻辑 两块:UI 负责交互和展示,统计逻辑单独放到 utils 里,后续无论你要做"关键词高亮""敏感词检测"都比较好接。

目标与口径

这里先把统计口径说清楚,不然后面很容易对不上:

  • 字符数text.length,包含空格、换行。
  • 无空白字符数:把所有空白(空格/换行/制表符等)去掉再计数。
  • 单词数:以连续空白分隔(适合英文/代码)。中文严格意义没有"单词",这里不做分词,保持工具定位简单。
  • 行数 :按 \n 统计。
  • 段落数:按"空行分隔"统计(两个换行或换行+空白+换行)。

1)先把统计逻辑抽出来

lib/utils/text_stats.dart

dart 复制代码
class TextStats {
  final int charCount;
  final int charNoSpaceCount;
  final int wordCount;
  final int lineCount;
  final int paragraphCount;

  const TextStats({
    required this.charCount,
    required this.charNoSpaceCount,
    required this.wordCount,
    required this.lineCount,
    required this.paragraphCount,
  });
}

说明

把统计结果做成一个不可变的 TextStats,好处挺实在:

  • 状态更干净 :页面只需要保存一个对象,不用维护一堆 int
  • 更容易测试:后面你可以单独写测试用例喂各种字符串。
  • 扩展简单:要加"中文标点数""数字个数",只是在这个类里多一个字段。

lib/utils/text_stats.dart

dart 复制代码
class TextStatsCalculator {
  static TextStats calc(String text) {
    final trimmed = text.trim();

    final charCount = text.length;
    final charNoSpaceCount = text.replaceAll(RegExp(r'\s'), '').length;

    final wordCount = trimmed.isEmpty
        ? 0
        : trimmed.split(RegExp(r'\s+')).where((e) => e.isNotEmpty).length;

    final lineCount = text.isEmpty ? 0 : text.split('\n').length;
    final paragraphCount = trimmed.isEmpty
        ? 0
        : trimmed.split(RegExp(r'\n\s*\n')).where((e) => e.trim().isNotEmpty).length;

    return TextStats(
      charCount: charCount,
      charNoSpaceCount: charNoSpaceCount,
      wordCount: wordCount,
      lineCount: lineCount,
      paragraphCount: paragraphCount,
    );
  }
}

说明

这里有几个小细节是实战里经常踩的:

  • 先保存 trimmed :避免反复 trim(),也把"只有空格"的情况统一处理。
  • where((e) => e.isNotEmpty) :有时候文本里会出现多个空格/制表符,直接 split 可能会产生空字符串。
  • 段落统计用 \n\s*\n:空行中间可能夹着空格,不处理会导致段落数偏大。

2)页面骨架:输入区 + 结果区

lib/pages/text_stats_page.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

import '../utils/text_stats.dart';

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

  @override
  State<TextStatsPage> createState() => _TextStatsPageState();
}

说明

入口这里我只保留最关键的东西:

  • ScreenUtil:做 Web/多端时字体、间距适配更省心。
  • utils 引入:统计逻辑不写在页面里,后面改规则不会影响 UI。

lib/pages/text_stats_page.dart

dart 复制代码
class _TextStatsPageState extends State<TextStatsPage> {
  final _controller = TextEditingController();
  TextStats _stats = const TextStats(
    charCount: 0,
    charNoSpaceCount: 0,
    wordCount: 0,
    lineCount: 0,
    paragraphCount: 0,
  );

  @override
  void initState() {
    super.initState();
    _controller.text = 'Flutter 是 Google 开发的开源 UI 框架。\n\n粘贴一段文本试试。';
    _recalculate();
  }

  void _recalculate() {
    setState(() => _stats = TextStatsCalculator.calc(_controller.text));
  }
}

说明

我一般会把页面状态收敛成一个对象:

  • _stats 初始值明确 :避免 build 过程中出现 null 或 "闪一下"。
  • _recalculate() 只做一件事 :把当前文本丢给 TextStatsCalculator,拿结果更新 UI。

3)输入框:不花哨,但要好用

lib/pages/text_stats_page.dart

dart 复制代码
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('文本统计')),
    body: Column(
      children: [
        Expanded(
          child: TextField(
            controller: _controller,
            maxLines: null,
            expands: true,
            style: TextStyle(fontSize: 14.sp),
            decoration: InputDecoration(
              hintText: '输入或粘贴文本...',
              border: InputBorder.none,
              contentPadding: EdgeInsets.all(16.w),
            ),
            onChanged: (_) => _recalculate(),
          ),
        ),
        _StatsPanel(stats: _stats),
      ],
    ),
  );
}

说明

这个输入区用的是我在工具类 App 里最常用的写法:

  • maxLines: null + expands: true:输入框自动撑满剩余空间,体验比滚动小框好很多。
  • onChanged 实时更新:这种统计类工具就应该"边输边变"。
  • 把结果面板抽出去 :页面 build 不会越来越臃肿。

4)统计面板:展示要清晰

lib/pages/text_stats_page.dart

dart 复制代码
class _StatsPanel extends StatelessWidget {
  final TextStats stats;
  const _StatsPanel({required this.stats});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16.w),
      color: Colors.grey[100],
      child: Column(
        children: [
          Row(children: [
            _StatItem(label: '字符数', value: stats.charCount, icon: Icons.text_fields),
            _StatItem(label: '无空白', value: stats.charNoSpaceCount, icon: Icons.space_bar),
          ]),
          SizedBox(height: 12.h),
          Row(children: [
            _StatItem(label: '单词数', value: stats.wordCount, icon: Icons.article),
            _StatItem(label: '行数', value: stats.lineCount, icon: Icons.format_list_numbered),
          ]),
        ],
      ),
    );
  }
}

说明

这里看起来"就是摆控件",但抽一层 _StatsPanel 有两个现实收益:

  • 方便复用:你后面要做"统计结果悬浮显示""侧边栏显示",直接复用这个组件。
  • 避免在页面里堆 UI:维护起来轻松很多。

lib/pages/text_stats_page.dart

dart 复制代码
class _StatItem extends StatelessWidget {
  final String label;
  final int value;
  final IconData icon;

  const _StatItem({required this.label, required this.value, required this.icon});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Card(
        child: Padding(
          padding: EdgeInsets.all(12.w),
          child: Row(
            children: [
              Icon(icon, color: Colors.blue),
              SizedBox(width: 8.w),
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.grey[600])),
                  Text('$value', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold)),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

说明

_StatItem 这种组件不要嫌麻烦,真实项目里几乎都会这么拆:

  • 视觉统一:所有统计项样式一致,不用每次手写一遍。
  • 后续改动集中:比如想把数字换成等宽字体、加一个点击复制,都只改这一个地方。

5)一些你可能会用到的小补充

如果你准备把它放进"Web 开发助手"这种工具集合里,我建议顺手加两点(不复杂,但很实用):

  • 清空按钮_controller.clear() 后调用 _recalculate()
  • 粘贴优化:Web 端用户常常是 Ctrl+V 连续粘贴多次,统计逻辑抽出来之后,你也可以很自然地做防抖(debounce)。

这两点我没有在上面的代码里硬塞进去,原因也很简单:文章里保持核心逻辑清晰,后面你扩展的时候不容易"牵一发动全身"。


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

相关推荐
光影少年2 小时前
智能体UI ux pro max
前端·ui·ux
半梅芒果干2 小时前
vue3 实现无缝循环滚动
前端·javascript·vue.js
qq_419854052 小时前
锚点跳转及鼠标滚动与锚点高亮联动
前端
冰敷逆向2 小时前
京东h5st纯算分析
java·前端·javascript·爬虫·安全·web
2501_940007892 小时前
Flutter for OpenHarmony三国杀攻略App实战 - 项目总结与未来展望
flutter
Laurence2 小时前
从零到一构建 C++ 项目(IDE / 命令行双轨实现)
前端·c++·ide
雯0609~3 小时前
hiprint-官网vue完整版本+实现客户端配置+可实现直接打印(在html版本增加了条形码、二维码拖拽等)
前端·javascript·vue.js
GISer_Jing3 小时前
构建高性能Markdown引擎开发计划
前端·aigc·ai编程
CHU7290353 小时前
生鲜商城小程序前端功能版块:适配生鲜采购核心需求
前端·小程序