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 接口:工具数据的建模
interfaceToolItem{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.tsexportconstTOOLS: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、卡片样式、渐变背景等核心知识点。但如果要上架应用商店,还需要补充搜索功能、工具分类、最近使用、收藏功能、路由传参等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。
