HarmonyOS ArkTS 工具网格与路由导航:从小工具百宝箱看卡片式布局与页面跳转的实战技巧
引子:一个应用塞五个小工具
"工具箱"应用——把二维码生成、单位换算、随机决策、颜色提取、倒计时五个小工具集成到一个应用里。需求很简单:首页放五个卡片,点击跳转到对应工具页面。
看起来就是"画五个卡片,加个跳转",但做起来发现几个细节要处理:卡片的阴影和圆角怎么配合、网格布局怎么自适应、路由跳转怎么传参、渐变背景怎么和卡片颜色协调。
这个应用虽然功能不复杂,但涉及了 ArkUI 开发的多个实用知识点。今天就从 Index.ets 的代码出发,聊聊工具网格的设计和路由导航的实现。
完整效果
项目结构:多页面的应用架构
和星空运势应用不同,这个工具箱是多页面架构——每个工具一个独立页面:
entry/src/main/ets/
├── pages/
│ ├── Index.ets # 首页(工具网格)
│ ├── QRGenerator.ets # 二维码生成
│ ├── UnitConverter.ets # 单位换算
│ ├── RandomTool.ets # 随机决策
│ ├── ColorPicker.ets # 颜色提取
│ └── Countdown.ets # 倒计时
├── entryability/
│ └── EntryAbility.ets
└── resources/
为什么用多页面而不是 Swiper
星空运势应用用 Swiper 在三个页面间切换,因为三个功能是并列的,用户可能频繁切换。但工具箱的五个工具是独立的——用户通常只用一个工具,不需要在工具间切换。
用路由跳转(router.pushUrl)更合适:
- 每个工具独立运行,不共享状态
- 跳转到工具页面后,用户可以返回首页再选另一个工具
- 页面栈管理更清晰(push 进去,pop 回来)
router 的引入
import { router } from '@kit.ArkUI';
router 是 ArkUI 的页面路由模块,提供页面跳转、返回、传参等功能。@kit.ArkUI 是鸿蒙系统的 UI 框架包,包含所有 UI 相关的 API。
ToolItem 接口:工具数据的建模
interface ToolItem {
name: string; // 工具名称
desc: string; // 工具描述
icon: string; // emoji 图标
page: string; // 跳转页面路径
color: string; // 卡片背景色
}

为什么用 interface 而不是 class
工具数据是只读的,不需要实例化方法,用 interface 比 class 更轻量。interface 编译后不会生成额外的 JavaScript 代码,包体积更小。
字段的设计考量
name:显示在卡片上的标题,简短有力desc:显示在卡片上的描述,说明工具用途icon:emoji 图标,比图片加载快,兼容性好page:路由路径,点击卡片时跳转到对应页面color:卡片背景色,每个工具有自己的主题色
为什么 color 不用枚举
颜色值用字符串而不是枚举,是因为:
- 灵活性:可以直接用 CSS 颜色值,不需要额外定义枚举
- 简洁性:
'#4F6EF7'比Color.BLUE更直观 - 扩展性:如果以后要加更多颜色,不需要修改枚举定义
工具数据:五个工具的定义
tools: ToolItem[] = [
{ name: '二维码生成', desc: '输入文字或链接,即时生成二维码', icon: '📱', page: 'pages/QRGenerator', color: '#4F6EF7' },
{ name: '单位换算', desc: '长度·重量·温度·面积·数据', icon: '📐', page: 'pages/UnitConverter', color: '#34C759' },
{ name: '随机决策', desc: '抛硬币·掷骰子·随机数·抽签', icon: '🎲', page: 'pages/RandomTool', color: '#FF6B35' },
{ name: '颜色提取', desc: '从图片取色,智能配色方案', icon: '🎨', page: 'pages/ColorPicker', color: '#AF52DE' },
{ name: '倒计时', desc: '设定事件日,实时倒数动画', icon: '⏳', page: 'pages/Countdown', color: '#FF2D55' },
];

数据和 UI 的分离
工具数据定义在组件内部(tools 数组),但和 UI 渲染逻辑分离。如果要加新工具,只需要在数组里加一个对象,不需要改 UI 代码。
颜色的选择
五个工具用了五种不同的颜色:
| 工具 | 颜色 | 色系 |
|---|---|---|
| 二维码生成 | #4F6EF7 | 蓝色 |
| 单位换算 | #34C759 | 绿色 |
| 随机决策 | #FF6B35 | 橙色 |
| 颜色提取 | #AF52DE | 紫色 |
| 倒计时 | #FF2D55 | 红色 |
五种颜色饱和度相近,在深色背景上都能看清,但又各有区分。用户可以通过颜色快速识别工具——“蓝色是二维码,绿色是换算”。
page 路径的格式
'pages/QRGenerator' 是路由路径,格式是 pages/页面名。不需要加 .ets 后缀,路由会自动查找对应的组件。
路由跳转:navTo 方法
navTo(page: string): void {
router.pushUrl({ url: page });
}
router.pushUrl 的用法
router.pushUrl 把目标页面压入页面栈,用户可以返回上一页。参数是一个对象,url 是目标页面的路径。
为什么封装成 navTo
直接调用 router.pushUrl({ url: item.page }) 也可以,但封装成 navTo 有两个好处:
- 语义化:
this.navTo(item.page)比router.pushUrl({ url: item.page })更易读 - 可扩展:如果以后要加路由拦截(比如登录检查),只需要改
navTo方法,不需要改所有 onClick
页面栈的管理
pushUrl 会把目标页面压入栈顶,用户按返回键会 pop 回首页。这是鸿蒙系统的标准导航模式,不需要额外处理。
如果要替换当前页面(不保留首页在栈中),可以用 router.replaceUrl。但工具箱需要保留首页,所以用 pushUrl。
页面布局:头部 + 网格
build() {
Column() {
// 头部
Column() {
Text('🧰 小工具百宝箱')
.fontSize(26).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('精选实用小工具,一个应用全搞定')
.fontSize(13).fontColor('#888888').margin({top:6})
}
.width('100%').padding({left:20,right:20,top:60,bottom:24})
.alignItems(HorizontalAlign.Start)
// 工具卡片网格
Scroll() {
Grid() { ... }
}
.layoutWeight(1)
.scrollBar(BarState.Off)
}
.width('100%').height('100%')
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#1A1A2E', 0.0], ['#16213E', 0.5], ['#0F3460', 1.0]]
})
}

头部区域
头部用 Column 居左对齐(alignItems(HorizontalAlign.Start)),包含标题和副标题:
- 标题:26px 粗体白色,醒目
- 副标题:13px 灰色,说明用途
- padding:top 60px 留出状态栏空间,bottom 24px 和网格隔开
网格区域
网格用 Scroll + Grid 实现:
Scroll:让网格可以滚动(工具数量多时)layoutWeight(1):占据剩余空间scrollBar(BarState.Off):隐藏滚动条
渐变背景
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#1A1A2E', 0.0], ['#16213E', 0.5], ['#0F3460', 1.0]]
})
背景是从深蓝到更深蓝的渐变:
- 顶部
#1A1A2E:最浅的深蓝色 - 中间
#16213E:中等深度 - 底部
#0F3460:最深的蓝色
这种渐变让页面有"深度感",比纯色背景更高级。
Grid 布局:2 列自适应
Grid() {
ForEach(this.tools, (item: ToolItem, idx: number) => {
GridItem() {
Column() {
Text(item.icon).fontSize(42).margin({bottom:14})
Text(item.name).fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({bottom:6})
Text(item.desc).fontSize(11).fontColor('#AAFFFFFF').maxLines(2).textAlign(TextAlign.Center)
}
.width('100%').height(160)
.borderRadius(18)
.backgroundColor(item.color)
.justifyContent(FlexAlign.Center)
.shadow({ radius: 8, color: item.color+'44', offsetY: 4 })
.onClick(() => this.navTo(item.page))
}
})
}
.columnsTemplate('1fr 1fr')
.columnsGap(14)
.rowsGap(14)
.padding({left:20,right:20,top:8,bottom:80})
.width('100%')

columnsTemplate 的用法
.columnsTemplate('1fr 1fr') 定义了两列等宽布局。1fr 表示占剩余空间的比例,两个 1fr 就是两列各占一半。
如果要三列,改成 '1fr 1fr 1fr'。如果要不等宽,可以用 '2fr 1fr'(第一列是第二列的两倍宽)。
间距的控制
columnsGap(14):列间距 14pxrowsGap(14):行间距 14pxpadding({left:20,right:20,top:8,bottom:80}):左右留 20px 边距,底部留 80px(避免被系统导航栏遮挡)
卡片的尺寸
每个卡片 width('100%').height(160),宽度自适应列宽,高度固定 160px。固定高度保证所有卡片高度一致,视觉上整齐。
卡片样式:圆角、阴影、背景色
圆角
.borderRadius(18) 给卡片 18px 的圆角。大圆角让卡片看起来更柔和,符合现代 UI 设计趋势。
阴影
.shadow({ radius: 8, color: item.color+'44', offsetY: 4 })
阴影有三个参数:
radius: 8:阴影模糊半径,越大越模糊color: item.color+'44':阴影颜色,用卡片颜色加 44(26% 透明度)offsetY: 4:阴影垂直偏移,让阴影在卡片下方
为什么阴影颜色用卡片颜色?
用灰色阴影(#00000044)也可以,但用卡片颜色的半透明版本更有"发光感"——蓝色卡片的阴影是蓝色的,绿色卡片的阴影是绿色的。
背景色
.backgroundColor(item.color) 用工具数据中的颜色作为卡片背景。每个工具有自己的主题色,用户可以通过颜色快速识别。
内容居中
.justifyContent(FlexAlign.Center) 让卡片内容垂直居中。图标、名称、描述三个元素在卡片中间排列,不会偏上或偏下。
卡片内容:图标、名称、描述
Column() {
Text(item.icon).fontSize(42).margin({bottom:14})
Text(item.name).fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({bottom:6})
Text(item.desc).fontSize(11).fontColor('#AAFFFFFF').maxLines(2).textAlign(TextAlign.Center)
}
三个元素的层次
| 元素 | 字体大小 | 颜色 | 作用 |
|---|---|---|---|
| 图标 | 42px | 默认 | 视觉焦点 |
| 名称 | 17px 粗体 | 白色 | 工具名称 |
| 描述 | 11px | 半透明白色 | 工具用途 |
图标最大,吸引注意力;名称粗体,告诉用户这是什么;描述最小,补充说明。
描述的截断
.maxLines(2) 限制描述最多显示两行,超过两行显示省略号。这样可以保证卡片高度一致,不会因为描述太长导致卡片参差不齐。
文字颜色
- 标题:纯白色
#FFFFFF,最醒目 - 描述:半透明白色
#AAFFFFFF,比标题弱化但比背景突出
踩坑记录
坑 1:Grid 的高度计算
Grid 的高度是自动计算的,但如果子组件高度不一致,可能导致布局错乱。代码中给每个 GridItem 设了固定高度(160),避免了这个问题。
坑 2:Scroll 的 scrollBar
默认情况下 Scroll 会显示滚动条,影响美观。.scrollBar(BarState.Off) 隐藏滚动条。
坑 3:router 的路径格式
路由路径不需要加 .ets 后缀。写 'pages/QRGenerator' 而不是 'pages/QRGenerator.ets'。
坑 4:底部 padding
网格底部留了 80px 的 padding,是为了避免被系统导航栏遮挡。如果底部工具卡片被导航栏挡住,可以增大这个值。
坑 5:渐变背景的方向
GradientDirection.Bottom 表示从上到下渐变。如果要从下到上,用 GradientDirection.Top。
代码改进建议
1. 工具数据外部化
当前工具数据写在组件内部。如果以后要加更多工具,建议移到独立文件:
// data/Tools.ts
export const TOOLS: ToolItem[] = [ ... ];
2. 卡片点击动画
当前卡片点击没有反馈。可以加缩放动画:
.scale({ x: this.scaleVal, y: this.scaleVal })
.animation({ duration: 150 })
.onClick(() => {
this.scaleVal = 0.95;
setTimeout(() => { this.scaleVal = 1; }, 150);
this.navTo(item.page);
})
3. 路由传参
如果要给工具页面传参(比如从首页传入初始值),可以用 router.pushUrl 的 params:
router.pushUrl({ url: page, params: { initialValue: 'xxx' } });
然后在目标页面用 router.getParams() 获取。
4. 页面栈管理
如果用户从工具页面返回首页后想清空页面栈(避免多次返回),可以用 router.clear()。
5. 搜索功能
如果工具数量增多,可以加搜索功能。用 TextInput 组件接收搜索关键词,过滤 tools 数组。
总结
小工具百宝箱的首页核心是"数据驱动网格"——把工具数据用 Grid 渲染成卡片,点击通过 router 跳转到对应页面。卡片的圆角、阴影、背景色配合渐变背景,视觉效果干净整洁。
适用边界:这个部分适合用作 ArkUI 工具网格和路由导航的学习案例,涵盖了 ToolItem 接口设计、Grid 布局、router.pushUrl、卡片样式、渐变背景等核心知识点。但如果要上架应用商店,还需要补充搜索功能、工具分类、最近使用、收藏功能、路由传参等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。
更多推荐



所有评论(0)