
快捷键能大幅提升开发效率。今天我们实现一个常用快捷键参考页面。
需求拆一下(做出来像"项目里的一个页面")
- 按工具分类:VS Code、Chrome DevTools、Git 等。
- 支持快速检索:输入关键词过滤(按键/描述都能搜)。
- 交互要顺手:点一下按键组合直接复制,省得手抄。
- 样式适配:Web / PC 屏幕宽度变化大,页面间距和字号最好做响应式。
下面我按项目里常见的拆法写:先定义数据结构,再做列表单元,最后拼成页面。
1) 数据结构:别用 Map<String, dynamic> 硬扛
dart
class ShortcutEntry {
final String keys;
final String desc;
const ShortcutEntry({
required this.keys,
required this.desc,
});
}
const Map<String, List<ShortcutEntry>> kShortcuts = {
'VS Code': [
ShortcutEntry(keys: 'Ctrl + P', desc: '快速打开文件'),
ShortcutEntry(keys: 'Ctrl + Shift + P', desc: '命令面板'),
ShortcutEntry(keys: 'Ctrl + /', desc: '切换注释'),
ShortcutEntry(keys: 'Alt + ↑/↓', desc: '移动行'),
ShortcutEntry(keys: 'Ctrl + D', desc: '选择下一个匹配项'),
],
'Chrome DevTools': [
ShortcutEntry(keys: 'F12', desc: '打开开发者工具'),
ShortcutEntry(keys: 'Ctrl + Shift + C', desc: '选择元素'),
ShortcutEntry(keys: 'Ctrl + Shift + J', desc: '打开控制台'),
ShortcutEntry(keys: 'Ctrl + R', desc: '刷新页面'),
],
};
说明
- [类型明确]
ShortcutEntry这个小模型能让代码更稳,后面做搜索、排序、埋点都方便。 - [数据可替换] 这里先用
const做内置数据;后续如果要从接口下发,把kShortcuts换成仓库层(Repository)返回的数据结构就行。
2) 列表单元:展示 + 一键复制
dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class ShortcutTile extends StatelessWidget {
final ShortcutEntry entry;
const ShortcutTile({
super.key,
required this.entry,
});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: ListTile(
onTap: () async {
await Clipboard.setData(ClipboardData(text: entry.keys));
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已复制:${entry.keys}')),
);
},
leading: Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
entry.keys,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12.sp,
fontWeight: FontWeight.w700,
),
),
),
title: Text(entry.desc),
trailing: Icon(Icons.copy, size: 18.sp, color: Colors.grey.shade600),
),
);
}
}
说明
- [点击复制] 用
Clipboard.setData是 Web/桌面常用的交互,配合SnackBar给到反馈,用户会觉得"这个页面能用"。 - [mounted 判断]
await之后补一个context.mounted,避免页面刚好被销毁时弹SnackBar报错,这个在真实项目里挺常见。 - [视觉层次] 左侧按键用等宽字体 + 浅灰底,扫一眼就能区分"按键组合"和"说明文字"。
3) 分组块:标题 + 列表
dart
class ShortcutSection extends StatelessWidget {
final String title;
final List<ShortcutEntry> items;
const ShortcutSection({
super.key,
required this.title,
required this.items,
});
@override
Widget build(BuildContext context) {
if (items.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 12.h),
child: Text(
title,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w800,
color: Colors.blue,
),
),
),
for (final entry in items) ShortcutTile(entry: entry),
SizedBox(height: 12.h),
],
);
}
}
说明
- [空分组直接隐藏] 搜索过滤后可能出现某些工具没有匹配项,
items.isEmpty时返回空控件,列表会更干净。 - [for 循环代替 map] 在 UI 代码里我更偏向用
for,断点调试更直观,阅读成本也低一些。
4) 页面本体:搜索过滤 + 渲染
dart
class ShortcutsPage extends StatefulWidget {
const ShortcutsPage({super.key});
@override
State<ShortcutsPage> createState() => _ShortcutsPageState();
}
class _ShortcutsPageState extends State<ShortcutsPage> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Map<String, List<ShortcutEntry>> _filter(String q) {
final query = q.trim().toLowerCase();
if (query.isEmpty) return kShortcuts;
return {
for (final e in kShortcuts.entries)
e.key: e.value
.where((it) =>
it.keys.toLowerCase().contains(query) ||
it.desc.toLowerCase().contains(query))
.toList(),
};
}
@override
Widget build(BuildContext context) {
final filtered = _filter(_controller.text);
return Scaffold(
appBar: AppBar(title: const Text('快捷键参考')),
body: ListView(
padding: EdgeInsets.all(16.w),
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
hintText: '搜索:比如 Ctrl、F12、控制台...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
),
isDense: true,
),
onChanged: (_) => setState(() {}),
),
SizedBox(height: 12.h),
for (final entry in filtered.entries)
ShortcutSection(title: entry.key, items: entry.value),
],
),
);
}
}
说明
- [过滤逻辑放在函数里]
_filter单独抽出来,后面要加"只看收藏""按热度排序"时不至于把build搞得一团乱。 - [过滤策略] 同时对
keys/desc过滤;并且用trim + toLowerCase,能覆盖大多数输入习惯。 - [不要一次性 setState 太重] 这里直接
onChanged => setState是最简单的写法;如果数据变多(比如几百条),再加个 debounce 就够用了。
5) 接入到项目:ScreenUtil 初始化(常见遗漏点)
dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
builder: (_, __) {
return MaterialApp(
home: const ShortcutsPage(),
);
},
);
}
}
说明
- [先初始化再用 .w/.h/.sp]
flutter_screenutil最容易踩坑的就是:没ScreenUtilInit就开始用16.w,结果某些端上尺寸会不对。 - [designSize 怎么选] 这里用常见的 iPhone X 尺寸只是示例;你项目里如果有设计稿基准,按设计稿来就行。
一点小经验(写在最后)
- [快捷键会变] 这类内容维护成本不低,最好把数据结构留好口子,后面能换成配置文件或后台下发。
- [复制反馈要轻]
SnackBar不要太重,也别弹 Dialog,不然用户会觉得"打断操作"。 - [Web 上更需要搜索] 屏幕能放很多内容,但用户更倾向于"搜一下马上找到",检索框属于必需品。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net