引子:一个应用塞五个小工具

"工具箱"应用——把二维码生成、单位换算、随机决策、颜色提取、倒计时五个小工具集成到一个应用里。需求很简单:首页放五个卡片,点击跳转到对应工具页面。

看起来就是"画五个卡片,加个跳转",但做起来发现几个细节要处理:卡片的阴影和圆角怎么配合、网格布局怎么自适应、路由跳转怎么传参、渐变背景怎么和卡片颜色协调。

这个应用虽然功能不复杂,但涉及了 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

工具数据是只读的,不需要实例化方法,用 interfaceclass 更轻量。interface 编译后不会生成额外的 JavaScript 代码,包体积更小。

字段的设计考量

  • name:显示在卡片上的标题,简短有力
  • desc:显示在卡片上的描述,说明工具用途
  • icon:emoji 图标,比图片加载快,兼容性好
  • page:路由路径,点击卡片时跳转到对应页面
  • color:卡片背景色,每个工具有自己的主题色

为什么 color 不用枚举

颜色值用字符串而不是枚举,是因为:

  1. 灵活性:可以直接用 CSS 颜色值,不需要额外定义枚举
  2. 简洁性'#4F6EF7'Color.BLUE 更直观
  3. 扩展性:如果以后要加更多颜色,不需要修改枚举定义

工具数据:五个工具的定义

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 有两个好处:

  1. 语义化this.navTo(item.page)router.pushUrl({ url: item.page }) 更易读
  2. 可扩展:如果以后要加路由拦截(比如登录检查),只需要改 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):列间距 14px
  • rowsGap(14):行间距 14px
  • padding({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.pushUrlparams

router.pushUrl({ url: page, params: { initialValue: 'xxx' } });

然后在目标页面用 router.getParams() 获取。

4. 页面栈管理

如果用户从工具页面返回首页后想清空页面栈(避免多次返回),可以用 router.clear()

5. 搜索功能

如果工具数量增多,可以加搜索功能。用 TextInput 组件接收搜索关键词,过滤 tools 数组。

总结

小工具百宝箱的首页核心是"数据驱动网格"——把工具数据用 Grid 渲染成卡片,点击通过 router 跳转到对应页面。卡片的圆角、阴影、背景色配合渐变背景,视觉效果干净整洁。

适用边界:这个部分适合用作 ArkUI 工具网格和路由导航的学习案例,涵盖了 ToolItem 接口设计、Grid 布局、router.pushUrl、卡片样式、渐变背景等核心知识点。但如果要上架应用商店,还需要补充搜索功能、工具分类、最近使用、收藏功能、路由传参等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐