
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