Flutter for OpenHarmony Web开发助手App实战:HTML参考

HTML 参考页这种功能,看起来简单,但实际用起来大家更在意两点:

  • [查得快] 能搜、能过滤,别翻半天。
  • [看得懂] 点进去有常见写法,最好还能一键复制。

下面用一个偏"项目里真的会这么写"的方式实现:把数据、UI、交互拆开写,代码也拆成小段,方便你直接搬到工程里。

1. 先把数据结构定下来

dart 复制代码
class HtmlTagItem {
  final String tag;
  final String desc;
  final String? example;

  const HtmlTagItem({
    required this.tag,
    required this.desc,
    this.example,
  });
}

说明

  • [为什么不直接用 Map] 页面一旦加了搜索/分组/收藏,Map 很容易变成"到处取字段、到处判空"。模型定好之后,后面加字段也更稳。
  • [example 做成可选] 不是每个标签都必须给示例,保留弹性。

2. 内置一份基础数据(后面可以替换成 JSON/接口)

dart 复制代码
const htmlTagSeed = <HtmlTagItem>[
  HtmlTagItem(tag: '<html>', desc: 'HTML 文档的根元素'),
  HtmlTagItem(tag: '<head>', desc: '文档头部,包含元数据'),
  HtmlTagItem(tag: '<body>', desc: '文档主体内容'),
  HtmlTagItem(tag: '<div>', desc: '块级容器元素', example: '<div class="box">...</div>'),
  HtmlTagItem(tag: '<span>', desc: '行内容器元素', example: '<span>text</span>'),
  HtmlTagItem(tag: '<a>', desc: '超链接', example: '<a href="https://example.com">Link</a>'),
  HtmlTagItem(tag: '<img>', desc: '图片', example: '<img src="/a.png" alt="logo" />'),
];

说明

  • [数据别写在 build 里] 写在 build() 里每次重建都会 new 一遍,列表一大就有点浪费。真实项目里一般把数据挪到独立文件或通过仓库层加载。
  • [先覆盖常用] 速查页不是百科,先把高频的写好,体验更重要。

3. 页面结构:搜索框 + 列表

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

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

  @override
  State<HtmlReferencePage> createState() => _HtmlReferencePageState();
}

class _HtmlReferencePageState extends State<HtmlReferencePage> {
  final _keywordCtrl = TextEditingController();
  String _keyword = '';

  @override
  void dispose() {
    _keywordCtrl.dispose();
    super.dispose();
  }
}

说明

  • [为什么用 Stateful] 搜索关键字是页面状态;如果你项目里用 Provider/Riverpod/BLoC,也可以把 _keyword 挪到状态管理里。
  • [dispose 习惯] TextEditingController 不释放,页面频繁进出时容易留下隐性泄漏。

4. 构建 UI:把过滤逻辑写清楚

dart 复制代码
@override
Widget build(BuildContext context) {
  final list = htmlTagSeed.where((e) {
    final k = _keyword.trim().toLowerCase();
    if (k.isEmpty) return true;
    return e.tag.toLowerCase().contains(k) || e.desc.toLowerCase().contains(k);
  }).toList(growable: false);

  return Scaffold(
    appBar: AppBar(title: const Text('HTML参考')),
    body: Column(
      children: [
        _buildSearchBar(),
        Expanded(child: _buildList(list)),
      ],
    ),
  );
}

说明

  • [过滤逻辑放在 build 里可以] 数据量不大时这样最直观;如果你后面要做拼音搜索、分组、收藏排序,再考虑抽成单独方法或用 debounce
  • [不要在 ListView 外面再套滚动] 这里用 Column + Expanded(ListView),避免滚动冲突。

5. 搜索框:做得"像工具"一点

dart 复制代码
Widget _buildSearchBar() {
  return Padding(
    padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
    child: TextField(
      controller: _keywordCtrl,
      onChanged: (v) => setState(() => _keyword = v),
      decoration: InputDecoration(
        hintText: '搜索标签或描述,比如 div / 表单',
        prefixIcon: const Icon(Icons.search),
        suffixIcon: _keyword.isEmpty
            ? null
            : IconButton(
                icon: const Icon(Icons.clear),
                onPressed: () {
                  _keywordCtrl.clear();
                  setState(() => _keyword = '');
                },
              ),
        border: const OutlineInputBorder(),
      ),
    ),
  );
}

说明

  • [清空按钮很关键] 这种工具页用户会连续搜很多次,有清空按钮会顺手很多。
  • [hintText 写"例子"] 比纯提示更有效,用户一眼知道能搜什么。

6. 列表渲染:抽一个 item,方便以后加收藏/右侧操作

dart 复制代码
Widget _buildList(List<HtmlTagItem> list) {
  return ListView.separated(
    padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
    itemCount: list.length,
    separatorBuilder: (_, __) => const SizedBox(height: 10),
    itemBuilder: (context, index) {
      final item = list[index];
      return _HtmlTagTile(
        item: item,
        onTap: () => _showTagDetail(item),
      );
    },
  );
}

说明

  • [separated 比 margin 更省心] 间距统一、视觉也更稳定。
  • [tile 抽出来] 后面你要加"长按复制""收藏星标""标签分类色条",直接改一个组件就行。

7. 列表项:标签用等宽字体,视觉更像"代码"

dart 复制代码
class _HtmlTagTile extends StatelessWidget {
  final HtmlTagItem item;
  final VoidCallback onTap;

  const _HtmlTagTile({required this.item, required this.onTap});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        onTap: onTap,
        leading: Container(
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
          decoration: BoxDecoration(
            color: Colors.orange.shade50,
            borderRadius: BorderRadius.circular(6),
          ),
          child: Text(
            item.tag,
            style: TextStyle(
              fontFamily: 'monospace',
              fontWeight: FontWeight.w700,
              color: Colors.orange.shade900,
            ),
          ),
        ),
        title: Text(item.desc),
        trailing: const Icon(Icons.chevron_right),
      ),
    );
  }
}

说明

  • [等宽字体] 速查页最怕"看起来像普通文本",用 monospace 会立刻有"代码感"。
  • [颜色别太重] 轻底色 + 深文字,既突出标签又不刺眼。

8. 点击详情:用 BottomSheet 更符合"工具页"的节奏

dart 复制代码
void _showTagDetail(HtmlTagItem item) {
  showModalBottomSheet(
    context: context,
    showDragHandle: true,
    builder: (context) {
      return Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(item.tag, style: const TextStyle(fontFamily: 'monospace', fontSize: 18)),
            const SizedBox(height: 8),
            Text(item.desc),
            if ((item.example ?? '').isNotEmpty) ...[
              const SizedBox(height: 12),
              const Text('常见写法', style: TextStyle(fontWeight: FontWeight.w600)),
              const SizedBox(height: 6),
              SelectableText(item.example!, style: const TextStyle(fontFamily: 'monospace')),
            ],
            const SizedBox(height: 12),
          ],
        ),
      );
    },
  );
}

说明

  • [用 BottomSheet 而不是新页面] 速查通常是"看一眼就走",底部弹层更轻量。
  • [SelectableText] 真实使用里很多人会复制示例去改,SelectableText 比普通 Text 更实用。

9. 一个小加分:支持"一键复制标签"

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

Future<void> copyToClipboard(BuildContext context, String text) async {
  await Clipboard.setData(ClipboardData(text: text));
  if (!context.mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('已复制到剪贴板')),
  );
}

说明

  • [SnackBar 别省] 复制成功/失败不给反馈,用户会以为没点到。
  • [mounted 判断] 弹层关闭很快时,异步回调里直接用 context 容易踩到"已卸载组件"的报错。

做到这里,一个"能搜、能点开看例子、能复制"的 HTML 参考页就够用了。后续如果你要继续迭代,我建议优先做:

  • [收藏] 常用标签置顶
  • [分类] 文档结构/文本/表单/媒体
  • [数据外置] 用 JSON 配置,更新不发版

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

相关推荐
霍理迪2 小时前
CSS移动端开发及less使用方法
前端·css
爱内卷的学霸一枚2 小时前
现代前端工程化实践:从Vue到React的架构演进与性能优化(7000字深度解析)
前端·vue.js·react.js
南风知我意9572 小时前
【前端面试4】框架以及TS
前端·面试·职场和发展
鹏北海-RemHusband2 小时前
踩坑记录:iOS Safari 软键盘下的“幽灵弹窗“问题
前端·ios·safari
一位搞嵌入式的 genius2 小时前
深入理解浏览器中的 JavaScript:BOM、DOM、网络与性能优化
前端·javascript·网络·性能优化
lang201509282 小时前
一键生成Java Web项目:Tomcat-Maven原型解析
java·前端·tomcat
We་ct2 小时前
LeetCode 242. 有效的字母异位词:解法解析与时空优化全攻略
前端·算法·leetcode·typescript
David凉宸2 小时前
Vue 3生态系统深度解析与最佳实践
前端·javascript·vue.js
全栈小52 小时前
【前端】win11操作系统安装完最新版本的NodeJs运行npm install报错,提示在此系统上禁止运行脚本
前端·npm·node.js