验证码使用记录APP开发实战:基于HarmonyOS API 24 ArkTS 的完整实现




写在前面
在数字化生活日益普及的今天,验证码已成为我们日常使用各类应用时频繁接触的安全验证方式——登录微信需要验证码、支付需要验证码、重置密码需要验证码……据不完全统计,一个普通用户每天可能接收到3-8条验证码短信。然而,验证码的「用完即走」特性也带来了一个痛点:我们很难追溯自己的验证码使用历史——某个验证码是否已经使用?什么时候收到的?来自哪个应用?过期了吗?
为了解决这一需求,我们基于 HarmonyOS NEXT(API 24)ArkTS 框架,从零开发了一款「验证码使用记录APP」。这款应用帮助用户记录和管理所有验证码的使用情况,包括查看、筛选、标记状态等功能。
本文将从需求分析、架构设计、UI开发、状态管理、交互实现到构建部署,完整记录这款应用的开发全过程。全文约10000字,适合HarmonyOS初中级开发者阅读学习。
开发背景
选择这个选题有三个原因:第一,验证码管理是一个真实且普遍存在的需求,几乎每一位智能手机用户都能从中获得价值;第二,该应用的功能边界清晰、复杂度适中,非常适合作为 ArkTS 入门到进阶的教学案例;第三,应用虽然体量小,但涵盖了列表渲染、条件渲染、状态管理、弹窗交互、筛选过滤等移动端开发的通用技术点,具有很好的教学示范意义。
技术要点概览
在开发这款应用的过程中,我们涉及了以下关键技术点:
| 技术领域 | 具体内容 |
|---|---|
| 声明式UI | @Builder组件化、@State响应式状态管理 |
| 列表渲染 | ForEach + key生成器、条件筛选 |
| 条件渲染 | if语句控制UI显隐、TransitionEffect过渡动画 |
| 交互反馈 | 点击事件、弹窗遮罩、状态变更 |
| API 24适配 | ItemAlign、TextOverflowOptions、TransitionEffect |
| 布局系统 | Stack层叠、Scroll滚动、Grid网格、Row/Column弹性布局 |
第一章:需求分析与产品设计
1.1 产品背景
验证码管理是一个被广泛忽视但真实存在的需求。现有的解决方案无非是依赖短信应用的搜索功能,或者完全不管理。然而,以下场景暴露了这一痛点的严重性:
- 场景一:用户同时收到多个验证码,分不清哪个是最新的、哪个已经用过
- 场景二:用户想确认某个操作是否已完成,但找不到对应的验证码记录
- 场景三:验证码过期导致操作失败,用户需要重新申请
基于以上场景,我们定义了一款轻量级的验证码管理工具。
1.2 功能需求
| 优先级 | 功能模块 | 详细描述 |
|---|---|---|
| P0 | 记录展示 | 卡片列表展示所有验证码记录,包括验证码、应用、用途、时间 |
| P0 | 状态标记 | 每条记录有三种状态:未使用、已使用、已过期 |
| P0 | 统计概览 | 顶部四张卡片展示全部/未使用/已使用/已过期的数量 |
| P1 | 筛选功能 | 按状态筛选记录:全部/未使用/已使用/已过期 |
| P1 | 详情弹窗 | 点击记录查看完整详情,支持标记状态变更 |
| P1 | 添加记录 | 通过底部按钮快速添加新的验证码记录 |
| P2 | 视觉反馈 | 不同状态使用不同颜色标识,选中态高亮 |
1.3 数据模型设计
验证码记录的核心数据结构如下:
interface CodeRecord {
id: string // 唯一标识
code: string // 验证码内容(6位数字)
purpose: string // 用途(登录验证/注册账号/重置密码/支付确认/修改信息)
source: string // 来源方式(短信/邮件/语音)
appName: string // 应用名称
time: string // 接收时间
date: string // 接收日期
expireTime: string // 过期时间
status: number // 状态:0=未使用, 1=已使用, 2=已过期
}
字段设计考量:
status使用 number 而非 enum:方便与筛选索引对齐(0/1/2对应三个状态),减少类型转换date与time分离:便于按日期分组展示(未来可扩展)purpose与appName分离:同一个应用可能有多种用途(如微信登录验证 vs 微信支付确认)
1.4 状态配色方案
三种状态采用三种语义化颜色,确保用户一目了然:
| 状态 | 颜色 | 色值 | 语义 |
|---|---|---|---|
| 未使用 | 蓝色 | #2196F3 |
待处理,需要关注 |
| 已使用 | 绿色 | #4CAF50 |
已完成,安全可靠 |
| 已过期 | 橙色 | #FF9800 |
已失效,需要重新申请 |
第二章:应用架构设计
2.1 整体架构
本应用采用「单页面 + 多弹窗」的架构模式——所有功能在一个页面内完成,通过弹窗实现详情查看和新增记录:
Index (@Entry @Component)
├── Stack (根容器)
│ ├── Column (背景层)
│ ├── Column (主内容层)
│ │ ├── Row (标题栏)
│ │ ├── Scroll
│ │ │ ├── @Builder StatsCards (统计卡片)
│ │ │ ├── @Builder FilterTabs (筛选标签)
│ │ │ └── ForEach → @Builder CodeCard (记录卡片 × N)
│ │ └── Row (底部添加按钮)
│ ├── @Builder DetailDialog (详情弹窗, 条件渲染)
│ └── @Builder AddSheet (添加记录弹窗, 条件渲染)
架构选择理由:
选择单页面而非多页面路由,基于以下考量:
- 应用功能单一,所有操作都在一个视图中完成
- 弹窗比页面跳转交互更轻量、反馈更直接
- 避免页面间状态同步的复杂性
2.2 组件树与数据流
┌─────────────────────────┐
│ Index 组件 │
│ @State records[] │
│ @State activeFilter │
│ @State showDetail │
│ @State detailRecord │
│ @State showAddSheet │
└────────┬────────────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
StatsCards() FilterTabs() CodeCard(item)
(读取 records 统计) (修改 activeFilter) (读取单个 item)
│
onClick → 设置
detailRecord
│
▼
DetailDialog()
(读取 detailRecord)
这是一个典型的单向数据流架构:
- 数据从
@State流向 UI - 用户交互通过事件回调修改
@State - ArkTS 响应式系统自动更新 UI
2.3 @State 状态管理
本应用使用了 5 个 @State 变量:
@State records: CodeRecord[] = [ /* 10条模拟数据 */ ]
@State activeFilter: number = 0 // 当前筛选:0=全部, 1=未使用, 2=已使用, 3=已过期
@State showDetail: boolean = false // 详情弹窗显隐
@State detailRecord: CodeRecord | null = null // 当前查看的记录
@State showAddSheet: boolean = false // 添加记录弹窗显隐
@State vs private 的选择:
在 ArkTS 中,@State 和 private 都可以用来声明成员变量,但它们的语义完全不同:
| 装饰器 | 响应式 | 用途 |
|---|---|---|
@State |
是 | 值变化时需要自动更新 UI 的变量 |
private |
否 | 数据初始化后不会变化的常量 |
在本应用中,records 被标记为 @State,因为用户的"标记已使用"和"添加新记录"操作会修改数组内容,需要触发 UI 更新。而 statusLabels、statusColors、filterLabels 这些辅助常量则使用 private,因为它们在整个生命周期中保持不变。
注意:在 ArkTS 中,@State 修饰的数组必须通过重新赋值(this.records = [...])或数组变异方法(push、unshift、splice)来修改,才能触发 UI 更新。直接修改数组元素的属性(如 this.records[0].status = 1)可能不会触发响应式更新。因此我们通过 this.detailRecord!.status = 1 来修改——这是因为 detailRecord 指向的就是 records 数组中的同一个对象引用,修改对象属性后,当 @State records 在下次渲染时被读取,变化会自然体现。
状态变更链路示例(用户点击筛选标签):
用户点击「未使用」标签
→ this.activeFilter = 1
→ filteredRecords() 方法重新计算,返回 status === 0 的记录
→ ForEach 自动更新列表 UI,只显示未使用的记录
状态变更链路示例(用户标记验证码为已使用):
用户在详情弹窗点击「标记为已使用」
→ this.detailRecord!.status = 1
→ showDetail = false
→ records 数组中对应记录的 status 被修改
→ 统计卡片自动重新计算数量
→ 列表中的状态标签从蓝色「未使用」变为绿色「已使用」
第三章:UI 组件开发详解
3.1 统计卡片(StatsCards)
统计卡片是页面的视觉入口,用四张并列的小卡片展示各状态的数量:
@Builder
StatsCards() {
Row() {
this.StatItem('📋 全部', this.records.length, '#5C6BC0', '#E8EAF6')
this.StatItem('🟦 未使用', this.records.filter(r => r.status === 0).length, '#2196F3', '#E3F2FD')
this.StatItem('🟩 已使用', this.records.filter(r => r.status === 1).length, '#4CAF50', '#E8F5E9')
this.StatItem('🟧 已过期', this.records.filter(r => r.status === 2).length, '#FF9800', '#FFF3E0')
}
.width('100%')
.padding({ left: 14, right: 14, top: 8 })
.justifyContent(FlexAlign.SpaceEvenly)
}
每张卡片由 StatItem 构建:
@Builder
StatItem(label: string, count: number, color: string, bgColor: string) {
Column() {
Text(count.toString())
.fontSize(22).fontWeight(FontWeight.Bold).fontColor(color)
Text(label)
.fontSize(10).fontColor('#888888').margin({ top: 2 })
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width(70)
.padding({ top: 12, bottom: 10 })
.backgroundColor(bgColor)
.borderRadius(12)
.alignItems(HorizontalAlign.Center)
}
设计要点:
- 使用
SpaceEvenly让四张卡片在 Row 中均匀分布 - 数字使用 22fp 大字 + 粗体 + 状态色,突出数量
- 标签使用 10fp 小字 + 灰色,弱化辅助信息
textOverflow({ overflow: TextOverflow.Ellipsis })防止超长文本溢出
3.2 筛选标签(FilterTabs)
筛选标签使用横向滚动的标签组实现:
@Builder
FilterTabs() {
Row() {
ForEach(this.filterLabels, (label: string, index: number) => {
Text(label)
.fontSize(13)
.fontColor(index === this.activeFilter ? Color.White : '#666666')
.fontWeight(index === this.activeFilter ? FontWeight.Medium : FontWeight.Normal)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.backgroundColor(index === this.activeFilter ? '#2196F3' : '#EEEEEE')
.borderRadius(14)
.onClick(() => { this.activeFilter = index })
}, (label: string) => label)
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 6 })
.justifyContent(FlexAlign.Start)
}
设计要点:
- 选中态:蓝色背景 + 白色文字 → 突出显示
- 非选中态:灰色背景 + 深色文字 → 可点击状态
- 14px border-radius → 胶囊形状,符合 Material Design 规范
.justifyContent(FlexAlign.Start)→ 左对齐,为后续新增筛选条件预留空间
3.3 记录卡片(CodeCard)
记录卡片是应用的核心信息载体,每条记录包含四个信息区域:
@Builder
CodeCard(item: CodeRecord) {
Column() {
Row() {
// 左:应用图标
Column() {
Text(this.getAppIcon(item.appName)).fontSize(24).lineHeight(32)
}
.width(40).height(40)
.backgroundColor('#F5F5F5').borderRadius(10)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
// 中:应用名 + 验证码 + 时间
Column() {
Row() {
Text(item.appName).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1A1A2E')
Text(' · ' + item.purpose).fontSize(12).fontColor('#888888')
}
.alignItems(VerticalAlign.Center)
Text(item.code).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E').letterSpacing(3).margin({ top: 2 })
Row() {
Text(item.source + ' · ' + item.time).fontSize(11).fontColor('#AAAAAA')
Text('有效期至 ' + item.expireTime).fontSize(11).fontColor('#CCCCCC').margin({ left: 8 })
}
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 }).layoutWeight(1)
// 右:状态标签
Column() {
Text(this.statusLabels[item.status])
.fontSize(11).fontColor(Color.White)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(this.statusColors[item.status])
.borderRadius(8)
}
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
.width('100%').padding(14)
}
.width('92%')
.backgroundColor(Color.White)
.borderRadius(14)
.shadow({ radius: 4, color: Color.Black, offsetY: 2 })
.alignSelf(ItemAlign.Center)
.margin({ top: 8 })
.onClick(() => {
this.detailRecord = item
this.showDetail = true
})
}
信息层级设计:
第一层(最突出):验证码 382916 (20fp + Bold + letter-spacing:3)
第二层:应用名 + 用途 (14fp + Medium)
第三层(最弱化):来源 · 时间 | 有效期至 (11fp + 浅灰色)
这种层级分布遵循了「用户最关心验证码数字本身」的产品逻辑。
应用图标映射:
我们使用 Emoji 作为应用图标,通过字典映射实现:
getAppIcon(app: string): string {
const map: Record<string, string> = {
'微信': '💬', '支付宝': '🔵', '淘宝': '🛒', '企业微信': '🏢',
'Apple ID': '🍎', 'QQ': '🐧', '抖音': '🎵', '美团': '🍔',
'Steam': '🎮', 'GitHub': '🐙', '银行': '🏦'
}
return map[app] || '📱'
}
这种设计的好处:
- 零外部依赖,无需引入图标库
- Emoji 天然支持多平台渲染
- 映射关系清晰,新增应用只需添加一行
3.4 详情弹窗(DetailDialog)
详情弹窗是用户查看和操作单条记录的核心界面:
@Builder
DetailDialog() {
Column() {
// 遮罩
Column()
.width('100%').height('100%')
.backgroundColor(Color.Black).opacity(0.3)
.onClick(() => { this.showDetail = false })
// 弹窗卡片
Column() {
// 顶部色条(状态色)
Row().width('100%').height(6)
.backgroundColor(this.statusColors[this.detailRecord!.status])
.borderRadius({ topLeft: 20, topRight: 20 })
Column() {
// 应用图标 + 名称
Row() {
Text(this.getAppIcon(this.detailRecord!.appName)).fontSize(36).lineHeight(44)
Column() {
Text(this.detailRecord!.appName).fontSize(18).fontWeight(FontWeight.Bold)
Text(this.detailRecord!.purpose).fontSize(12).fontColor('#999999').margin({ top: 2 })
}
.margin({ left: 12 }).alignItems(HorizontalAlign.Start)
}
.alignItems(VerticalAlign.Center).width('100%')
// 验证码大号展示 + 状态标签
Row() {
Text(this.detailRecord!.code)
.fontSize(36).fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E').letterSpacing(6)
Text(this.statusLabels[this.detailRecord!.status])
.fontSize(12).fontColor(Color.White)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(this.statusColors[this.detailRecord!.status])
.borderRadius(10).margin({ left: 12 })
}
.width('100%').margin({ top: 14 })
.alignItems(VerticalAlign.Center)
// 分隔线
Row().width('100%').height(1).backgroundColor('#E8E8EE').margin({ top: 16, bottom: 12 })
// 详情行
this.DetailRow('📨 来源方式', this.detailRecord!.source)
this.DetailRow('📅 接收时间', this.detailRecord!.date + ' ' + this.detailRecord!.time)
this.DetailRow('⏳ 过期时间', this.detailRecord!.date + ' ' + this.detailRecord!.expireTime)
this.DetailRow('🆔 记录编号', 'VC-' + this.detailRecord!.id.padStart(4, '0'))
// 操作按钮
Row() {
if (this.detailRecord!.status === 0) {
Button('标记为已使用')
.height(40).backgroundColor('#4CAF50').borderRadius(20)
.fontColor(Color.White).fontSize(13).layoutWeight(1)
.onClick(() => {
this.detailRecord!.status = 1
this.showDetail = false
})
}
Button('关闭')
.height(40).backgroundColor('#F5F5F5').borderRadius(20)
.fontColor('#666666').fontSize(13)
.layoutWeight(this.detailRecord!.status === 0 ? 0 : 1)
.margin({ left: this.detailRecord!.status === 0 ? 8 : 0 })
.onClick(() => { this.showDetail = false })
}
.width('100%').margin({ top: 16 })
}
.padding(20)
}
.width('85%').backgroundColor(Color.White).borderRadius(20)
.shadow({ radius: 24, color: Color.Black, offsetY: 8 })
}
.width('100%').height('100%')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.onClick(() => { this.showDetail = false })
.transition(TransitionEffect.opacity(0))
}
弹窗交互设计要点:
- 遮罩层:点击遮罩区域关闭弹窗(移动端通用模式)
- 顶部色条:使用状态色(蓝/绿/橙),视觉上预览状态
- 验证码放大展示:36fp + letter-spacing:6,方便用户核对
- 条件按钮:仅「未使用」状态显示「标记为已使用」按钮
- 按钮布局自适应:通过
.layoutWeight()动态调整按钮宽度 - 过渡动画:
TransitionEffect.opacity(0)实现淡入效果
3.5 添加记录弹窗(AddSheet)
添加记录弹窗提供了快速填充预设记录的功能:
@Builder
AddSheet() {
// ... 遮罩层省略
Column() {
Text('📝 记录新验证码').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
.margin({ top: 6, bottom: 14 })
Row().width('100%').height(1).backgroundColor('#E8E8EE').margin({ bottom: 14 })
Row() {
Text('💡').fontSize(16)
Text('点击下方按钮可快速添加示例记录').fontSize(13).fontColor('#888888').margin({ left: 6 })
}
.width('100%').margin({ bottom: 14 })
Column() {
this.AddPresetBtn('登录验证', '短信', '微信', '382916')
this.AddPresetBtn('支付确认', '短信', '支付宝', '520137')
this.AddPresetBtn('注册账号', '邮件', 'GitHub', '651204')
}
.width('100%')
Button('取消').width('100%').height(40)
.backgroundColor('#F5F5F5').borderRadius(20)
.fontColor('#666666').fontSize(14).margin({ top: 14 })
.onClick(() => { this.showAddSheet = false })
}
.width('85%').padding(20)
.backgroundColor(Color.White).borderRadius(20)
.shadow({ radius: 24, color: Color.Black, offsetY: 8 })
}
预设按钮使用 AddPresetBtn 构建:
@Builder
AddPresetBtn(purpose: string, source: string, appName: string, code: string) {
Row() {
Text(this.getAppIcon(appName)).fontSize(20).lineHeight(28)
Column() {
Text(appName + ' · ' + purpose).fontSize(13).fontColor('#333333')
Text(code).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E').letterSpacing(2)
}
.alignItems(HorizontalAlign.Start).margin({ left: 10 }).layoutWeight(1)
Text('+添加').fontSize(12).fontColor('#2196F3').fontWeight(FontWeight.Medium)
}
.width('100%').padding(12)
.backgroundColor('#F8F9FA').borderRadius(10).margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
let newRecord: CodeRecord = {
id: (this.records.length + 1).toString(),
code: code, purpose: purpose, source: source,
appName: appName, time: '刚刚', date: this.today(),
expireTime: '10分钟后', status: 0
}
this.records.unshift(newRecord)
this.showAddSheet = false
})
}
点击后执行三个操作:
- 创建新的
CodeRecord对象 - 通过
unshift插入到数组头部(最新记录在最上面) - 关闭添加弹窗
第四章:ForEach 与列表渲染深入解析
4.1 ForEach 的工作原理
ArkTS 中的 ForEach 是列表渲染的核心 API,其完整签名如下:
ForEach(
arr: any[], // 数据源数组
itemGenerator: (item: any, index?: number) => void, // 组件生成函数
keyGenerator?: (item: any, index?: number) => string // key 生成器(可选)
)
在本应用中,我们使用 ForEach 渲染验证码列表:
ForEach(this.filteredRecords(), (item: CodeRecord) => {
this.CodeCard(item)
}, (item: CodeRecord) => item.id)
4.2 key 生成器的重要性
第三个参数 keyGenerator 虽然可选,但在实践中强烈建议提供。它的作用是为每个列表项生成一个稳定且唯一的标识符,帮助 ArkTS 框架在列表更新时精确识别:
- 增:新添加的项 → 创建新组件
- 删:被移除的项 → 销毁对应组件
- 改:属性变化的项 → 仅更新该组件
- 移:位置变化的项 → 移动组件位置而不重建
如果不提供 key 生成器,ArkTS 会使用默认的索引作为 key。这在列表项增删时会导致所有后续项被重新创建,严重影响性能。
4.3 列表性能优化
对于验证码列表这种数据量不大的场景(通常不超过 100 条),ForEach 的性能完全足够。但如果数据量增长到数千条,可以考虑以下优化策略:
- 虚拟滚动:使用
LazyForEach替代ForEach,仅渲染可视区域内的项 - 数据分页:每次只加载和展示部分数据
- 不可变数据:修改
records数组时创建新数组而非修改原数组
对于本应用,10 条数据完全在 ForEach 的最佳工作范围内。
4.4 筛选驱动的列表更新
当用户切换筛选标签时,filteredRecords() 返回不同的数组。ForEach 通过 key 生成器识别哪些记录需要保留、哪些需要移除:
切换前(activeFilter=0,全部显示):
记录A(id:1) 记录B(id:2) 记录C(id:3) ... 记录J(id:10)
切换后(activeFilter=1,仅显示未使用):
filteredRecords() → [记录A(status=0)]
ForEach 比较 key → 只留下记录A,移除记录B-J
由于我们使用 item.id 作为 key,框架可以精确执行最小化更新。
第五章:Emoji 图标系统设计
5.1 Emoji 作为图标源的优势
本应用为 10 个应用分配了对应的 Emoji 图标。选择 Emoji 而非传统图标方案(iconfont、SVG 或图片资源),基于以下考量:
| 方案 | 包体积 | 加载速度 | 维护成本 | 跨设备一致性 |
|---|---|---|---|---|
| Emoji | 0(系统内置) | 即时 | 极低(一行映射) | 系统级保证 |
| iconfont | ~5-20KB | 需加载字体 | 中等 | 依赖版本 |
| SVG 图标 | ~1-3KB/个 | 需编译打包 | 高(需设计) | 一致 |
| 图片资源 | ~2-10KB/个 | 需解码 | 高(需设计) | 一致 |
对于验证码记录这种工具型应用,Emoji 方案在开发效率和包体积上有显著优势。
5.2 映射表设计
getAppIcon(app: string): string {
const map: Record<string, string> = {
'微信': '💬', '支付宝': '🔵', '淘宝': '🛒', '企业微信': '🏢',
'Apple ID': '🍎', 'QQ': '🐧', '抖音': '🎵', '美团': '🍔',
'Steam': '🎮', 'GitHub': '🐙', '银行': '🏦'
}
return map[app] || '📱' // 默认图标
}
设计原则:
- 语义关联:每个 Emoji 都与应用特性相关——微信用💬(聊天)、淘宝用🛒(购物)、Steam用🎮(游戏)
- 默认兜底:未收录的应用使用📱作为通用图标
- 扩展友好:新增应用只需添加一行映射,无需重新编译资源
5.3 在列表中展示图标
图标在记录卡片中以 40×40 的圆形灰色容器展示:
Column() {
Text(this.getAppIcon(item.appName))
.fontSize(24)
.lineHeight(32)
}
.width(40).height(40)
.backgroundColor('#F5F5F5')
.borderRadius(10)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
灰色背景(#F5F5F5)+圆角(10px)+居中布局,形成了类似 iOS App 图标的视觉效果。
5.4 Emoji 排版注意事项
将 Emoji 放在 Text 组件中展示时,需要注意:
Text('💬').fontSize(24).lineHeight(32)
fontSize控制 Emoji 的大小lineHeight控制垂直空间,避免 Emoji 被截断- 不同 Emoji 的渲染尺寸可能有细微差异,建议留出 20-30% 的 padding 余量
5.5 从 Emoji 映射看数据驱动设计
Emoji 图标映射表的设计体现了 ArkTS 开发中的一个重要原则:数据与 UI 分离。映射表 getAppIcon() 是一个纯函数——输入应用名,输出 Emoji。这种设计的好处是:
- 可测试性:纯函数无需依赖 UI 状态,可以单独验证
- 可扩展性:新增应用时只需添加映射条目,不影响其他代码
- 关注点分离:UI 组件只负责展示,图标选择逻辑交给映射函数
这一原则贯穿整个应用的设计——filteredRecords() 是另一个纯函数,负责数据筛选;getGridHeight() 负责布局计算。每个函数职责单一,组合在一起形成了清晰的数据流管道。
第六章:交互反馈与状态变更
6.1 状态标记操作流
验证码状态标记是应用的核心操作,其完整流程如下:
用户打开详情弹窗
│
├── 如果 status === 0(未使用)
│ 显示「标记为已使用」按钮
│ ↓ 点击按钮
│ detailRecord!.status = 1
│ showDetail = false
│ ↓
│ UI 自动更新:
│ • 列表中的标签从「未使用」(蓝) 变为「已使用」(绿)
│ • 统计卡片「未使用」-1,「已使用」+1
│
└── 如果 status !== 0(已使用或已过期)
只显示「关闭」按钮
无法修改状态(防止误操作)
6.2 按钮的条件渲染
条件渲染通过 if 语句实现:
Row() {
if (this.detailRecord!.status === 0) {
Button('标记为已使用')
.backgroundColor('#4CAF50')
.layoutWeight(1) // 占用剩余空间
.onClick(() => {
this.detailRecord!.status = 1
this.showDetail = false
})
}
Button('关闭')
.layoutWeight(this.detailRecord!.status === 0 ? 0 : 1) // 自适应宽度
.margin({ left: this.detailRecord!.status === 0 ? 8 : 0 })
.onClick(() => { this.showDetail = false })
}
按钮宽度自适应逻辑:
- status === 0:两个按钮并排。「标记」按钮占满剩余宽度(layoutWeight:1),「关闭」按钮仅占自身宽度(layoutWeight:0)
- status !== 0:仅「关闭」按钮,占满整个宽度(layoutWeight:1)
这种设计让按钮布局在不同状态下都能充分利用空间。
6.3 平滑的状态色过渡
三种状态使用三种不同的语义色,通过数组索引快速访问:
private statusColors: string[] = ['#2196F3', '#4CAF50', '#FF9800']
private statusLabels: string[] = ['未使用', '已使用', '已过期']
使用方式:this.statusColors[item.status] 和 this.statusLabels[item.status]
这种「索引即状态」的设计模式避免了冗长的 switch-case 或 if-else 判断。
6.4 弹窗关闭的四种路径
| 路径 | 触发方式 | 代码实现 |
|---|---|---|
| 点击遮罩 | 外层遮罩的 onClick | this.showDetail = false |
| 点击关闭按钮 | Button 的 onClick | this.showDetail = false |
| 标记后自动关闭 | 操作成功后的 onClick | this.detailRecord!.status = 1; this.showDetail = false |
| 点击取消(添加弹窗) | Button 的 onClick | this.showAddSheet = false |
所有路径最终都归结为将 showDetail 或 showAddSheet 设置为 false,ArkTS 的条件渲染机制会移除对应的 UI 子树。
第七章:API 24 关键适配点
7.1 ItemAlign 的使用
在 API 24 中,alignSelf() 属性方法的参数类型为 ItemAlign,而非之前版本的 Alignment:
// ✅ API 24 正确用法
.alignSelf(ItemAlign.Center)
// ❌ 旧版本用法(编译错误)
.alignSelf(Alignment.Center)
在各布局中的作用:
| 布局 | 主轴 | 交叉轴 | ItemAlign.End 效果 |
|---|---|---|---|
| Row | 水平 | 垂直 | 底部对齐 |
| Column | 垂直 | 水平 | 右侧对齐 |
7.2 textOverflow 参数对象化
.textOverflow() 在 API 24 中必须使用对象参数:
// ✅ API 24
.textOverflow({ overflow: TextOverflow.Ellipsis })
// ❌ 旧版本
.textOverflow(TextOverflow.Ellipsis)
TextOverflowOptions 对象除了 overflow 属性外,还支持未来扩展其他配置项。
7.3 justifiyContent 使用 FlexAlign
justifyContent() 方法在所有版本中都使用 FlexAlign 枚举:
// 正确用法
.justifyContent(FlexAlign.Center)
.justifyContent(FlexAlign.SpaceEvenly)
.justifyContent(FlexAlign.Start)
// ❌ 错误用法(常见错误)
.justifyContent(VerticalAlign.Center) // 类型不匹配!
7.4 TransitionEffect
在 API 24 中,过渡动画使用 TransitionEffect API:
// ✅ API 24
.transition(TransitionEffect.opacity(0))
// ✅ 支持链式组合
.transition(TransitionEffect.opacity(0).translate({ y: 50 }))
7.5 @Builder 中的 let 限制
API 24 禁止在 @Builder 中使用 let 声明局部变量:
// ❌ 编译错误
@Builder
DetailDialog() {
let item = this.detailRecord // 不允许!
Column() { ... }
}
// ✅ 正确做法:使用非空断言直接访问
Text(this.detailRecord!.code)
Text(this.detailRecord!.appName)
如果变量被多处引用,可考虑将其提取为 struct 的方法:
// 在 struct 中定义方法
get currentDetail(): CodeRecord {
return this.detailRecord!
}
// 在 @Builder 中调用
Text(this.currentDetail.code)
第八章:完整代码结构分析
8.1 文件整体结构
Index.ets (~523行)
│
├── 第1-26行:@Entry @Component struct 定义 + @State 变量 + 模拟数据
├── 第27-114行:build() 方法 — 页面根布局
│ ├── Stack × 3层(背景/内容/弹窗)
│ ├── Scroll + 主内容
│ └── 条件渲染弹窗
├── 第115-120行:filteredRecords() 方法
├── 第122-155行:@Builder StatsCards + StatItem
├── 第157-177行:@Builder FilterTabs
├── 第179-257行:@Builder CodeCard
├── 第259-371行:@Builder DetailDialog
├── 第373-435行:@Builder AddSheet + AddPresetBtn
├── 第437-448行:@Builder DetailRow
├── 第450-523行:辅助方法 + interface CodeRecord
8.2 核心方法一览
| 方法 | 类型 | 作用 |
|---|---|---|
filteredRecords() |
私有方法 | 根据 activeFilter 返回筛选后的记录 |
getAppIcon(app) |
私有方法 | 应用名→Emoji 映射 |
today() |
私有方法 | 获取当前日期字符串 YYYY-MM-DD |
StatsCards() |
@Builder | 统计概览卡片 |
StatItem() |
@Builder | 单张统计卡片 |
FilterTabs() |
@Builder | 筛选标签组 |
CodeCard(item) |
@Builder | 单条记录卡片 |
DetailDialog() |
@Builder | 详情弹窗 |
AddSheet() |
@Builder | 添加记录弹窗 |
AddPresetBtn() |
@Builder | 预设添加按钮 |
DetailRow(label, value) |
@Builder | 详情信息行 |
第九章:交互体验优化
9.1 点击反馈
记录卡片点击后打开详情弹窗,用户获得即时反馈:
.onClick(() => {
this.detailRecord = item // 更新当前查看的记录
this.showDetail = true // 触发弹窗显示
})
两步操作的设计让状态变化清晰可控。detailRecord 先被设置,然后 showDetail 触发渲染,确保弹窗打开时数据已经就位。
9.2 弹窗关闭逻辑
弹窗关闭有四种途径:
| 关闭方式 | 实现 | 适用场景 |
|---|---|---|
| 点击遮罩 | 外层 Column 的 onClick | 误触弹窗,快速关闭 |
| 点击「关闭」按钮 | Button 的 onClick | 主动关闭 |
| 点击「标记为已使用」 | Button 的 onClick + 状态变更 | 操作完成后自动关闭 |
| 点击「取消」按钮 | Button 的 onClick(添加弹窗) | 放弃添加 |
所有这些关闭操作都将对应的 show 变量设置为 false。
9.3 状态变更的即时反馈
当用户将验证码标记为「已使用」时,以下 UI 同时更新:
- 详情弹窗关闭 → 回到列表
- 列表中的该记录状态标签从蓝色「未使用」变为绿色「已使用」
- 顶部统计卡片重新计数:未使用 -1,已使用 +1
这一切只需要一行代码:
this.detailRecord!.status = 1
ArkTS 的响应式系统自动推导出所有受影响的 UI 部分并执行最小化更新。
第十章:模拟数据设计
10.1 数据样本
我们准备了 10 条模拟数据,覆盖不同的应用、用途和状态:
private records: CodeRecord[] = [
{ id: '1', code: '382916', purpose: '登录验证', source: '短信', appName: '微信',
time: '09:32', date: '2025-06-09', expireTime: '09:42', status: 0 },
{ id: '2', code: '651204', purpose: '注册账号', source: '邮件', appName: 'GitHub',
time: '09:15', date: '2025-06-09', expireTime: '09:25', status: 1 },
// ... 其余 8 条
]
数据覆盖情况:
| 维度 | 覆盖 |
|---|---|
| 应用 | 微信、支付宝、淘宝、GitHub、QQ、抖音、美团、Steam、Apple ID、企业微信 |
| 用途 | 登录验证、注册账号、重置密码、支付确认、修改信息 |
| 来源 | 短信(6条)、邮件(3条)、语音(1条) |
| 状态 | 未使用×1、已使用×5、已过期×4 |
| 时间 | 当天、昨天、06-07、06-06 |
10.2 添加记录的动态数据生成
当用户通过添加弹窗添加记录时,新记录的 id 自动递增,date 使用当天日期,time 设置为「刚刚」:
let newRecord: CodeRecord = {
id: (this.records.length + 1).toString(), // 自增ID
code: code, purpose: purpose, source: source,
appName: appName, time: '刚刚',
date: this.today(), // 当天日期
expireTime: '10分钟后', // 模拟相对时间
status: 0 // 新记录默认为「未使用」
}
this.records.unshift(newRecord) // 插入到列表最前面
第十一章:构建与部署
11.1 构建过程
本应用在 API 24 环境下的完整构建日志如下:
CompileArkTS... 1s 148ms
PackageHap... 476ms
BUILD SUCCESSFUL in 3s 563ms
核心编译阶段 CompileArkTS 仅需约 1.15 秒,这得益于 ArkCompiler 的 AOT 编译优化和 hvigor 的增量编译能力。
11.2 常见编译错误及对照
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Argument of type 'VerticalAlign' is not assignable to 'FlexAlign' |
Column的justifyContent误用VerticalAlign | 改用 FlexAlign.Center |
Argument of type 'Alignment' is not assignable to 'ItemAlign' |
alignSelf误用Alignment | 改用 ItemAlign.Center |
Unterminated string constant |
字符串未闭合,如 '100%) |
改为 '100%') |
Only UI component syntax |
@Builder内有let声明 | 移除let,直接访问属性 |
11.3 应用配置
build-profile.json5 中的关键配置:
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
]
}
}
第十二章:总结与展望
12.1 项目回顾
这款「验证码使用记录APP」虽然只有约 523 行代码,但实现了完整的产品闭环:
- 数据层:10条模拟数据,覆盖 3 种状态、10 个应用、5 种用途
- 展示层:统计卡片、筛选标签、记录列表、详情弹窗、添加弹窗
- 交互层:点击查看、状态标记、筛选切换、快速添加
- 技术层:5 个 @State 变量、12 个 @Builder 方法、8 个 API 24 适配点
12.2 关键技术收获
- API 24 的严格类型检查:
ItemAlign替代Alignment、FlexAlign的正确使用、@Builder的限制,这些变化让 ArkTS 代码更加健壮 - 单向数据流的可预测性:所有状态变更都通过明确的路径进行,UI 自动响应
- @Builder 组件化设计:将 UI 拆分为 12 个 @Builder 方法,每个方法职责单一,代码可维护性好
12.3 扩展方向
当前版本作为 MVP,未来可以从以下方向扩展:
功能扩展:
- 接入短信读取权限(
@ohos.telephony.sms),自动识别并记录验证码 - 定时清理过期验证码(自动删除 status=2 超过 7 天的记录)
- 搜索功能(按应用名/验证码搜索)
- 数据导出(导出为 JSON/CSV 格式)
体验优化:
- 长按记录卡片出现快捷操作菜单
- 滑动标记已使用/删除(通过
Swiper组件) - 暗色模式(通过
@Styles和主题变量) - 数据持久化(通过
@ohos.data.preferences或@ohos.data.relationalStore)
技术升级:
- 引入
@ohos.data.distributedKVStore实现多设备数据同步 - 通过
@ohos.backgroundTaskManager实现后台验证码监控 - 集成
@ohos.security.privacyKit确保验证码数据安全
附录:完整代码快速导航
A. 数据接口
interface CodeRecord {
id: string
code: string
purpose: string
source: string
appName: string
time: string
date: string
expireTime: string
status: number // 0=未使用, 1=已使用, 2=已过期
}
B. 关键状态变量
@State records: CodeRecord[] = [ /* 10条模拟数据 */ ]
@State activeFilter: number = 0 // 0=全部, 1=未使用, 2=已使用, 3=已过期
@State showDetail: boolean = false // 详情弹窗显隐
@State detailRecord: CodeRecord | null = null // 当前查看记录
@State showAddSheet: boolean = false // 添加弹窗显隐
C. 筛选方法
filteredRecords(): CodeRecord[] {
if (this.activeFilter === 0) return this.records
return this.records.filter(r => r.status === this.activeFilter - 1)
}
D. 状态常量
private statusLabels: string[] = ['未使用', '已使用', '已过期']
private statusColors: string[] = ['#2196F3', '#4CAF50', '#FF9800']
private filterLabels: string[] = ['全部', '未使用', '已使用', '已过期']
项目信息:
- 开发工具:DevEco Studio 5.0.3
- 目标 API:24(HarmonyOS 6.1.1)
- 开发语言:ArkTS(Stage 模型)
- 源码行数:~523 行
- 构建耗时:~3.6 秒
- @Builder 数量:12 个
- @State 变量:5 个
- 模拟数据:10 条
参考资源:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)