鸿蒙比价工具 - 历史价格查询 省钱购物助手


1. 应用概述
价格对比(PriceCompare)是一款基于HarmonyOS ArkTS框架开发的购物辅助工具,其核心功能是帮助用户在多个电商平台或店铺之间对比同类商品的价格,从而做出更明智的购买决策。这款应用支持记录商品在不同店铺的价格,自动标记更低价格的店铺,并保存价格对比历史供后续参考。
从技术实现角度来看,价格对比应用充分利用了ArkUI框架的声明式UI开发能力。通过@State装饰器管理商品对比数据,通过TextInput组件获取用户输入,通过List组件和ForEach循环渲染历史对比记录,通过条件渲染实现价格高低的视觉区分。应用的代码结构简洁明了,展示了购物比价类应用的核心开发模式。
本技术博客将从应用架构设计、核心代码实现、状态管理机制、UI渲染流程、交互设计等多个维度,对这款价格对比应用进行全面的技术剖析。通过本文的深入讲解,读者能够掌握HarmonyOS ArkTS开发中的核心知识点,特别是表单输入处理、数组状态管理、条件渲染实现、列表展示技术以及购物比价类应用的设计要点。
2. 技术架构分析
2.1 整体架构设计
价格对比应用采用了标准的单页面架构,整个应用仅包含一个主页面,通过垂直布局的Column容器组织各个功能区块。从代码组织角度来看,应用主要分为以下几个核心部分:页面入口组件(PriceCompare)、通用标题栏组件(CommonTitleBar)、输入区域(商品名称和两个店铺价格输入框)、对比操作按钮、对比结果列表区域。
┌─────────────────────────────────────────────────────────┐
│ PriceCompare页面 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ CommonTitleBar组件 │ │
│ │ (导航栏+标题显示) │ │
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 商品名称输入区域 │ │
│ │ (TextInput组件) │ │
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 价格对比输入区域 │ │
│ │ (店铺A价格 VS 店铺B价格) │ │
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 添加对比按钮 │ │
│ │ (Button组件) │ │
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 对比历史列表区域 │ │
│ │ (List + ForEach渲染) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
价格对比应用的UI结构清晰,遵循了从上到下的信息流布局模式,便于用户按照"输入商品→输入价格→查看对比结果"的逻辑流程进行操作。
2.2 模块依赖关系
从依赖关系的角度分析,价格对比应用主要依赖以下几个核心模块:
首先是CommonTitleBar组件,该组件提供了统一的页面标题栏和返回按钮功能,是应用导航结构的基础支撑。
其次是ArkUI的基础组件,这些组件构成了应用UI的原子化单元:
| 组件类型 | 组件名称 | 在本应用中的用途 |
|---|---|---|
| 布局容器 | Column | 垂直堆叠各个功能区块 |
| 布局容器 | Row | 水平排列价格对比输入框 |
| 文本输入 | TextInput | 输入商品名称和价格 |
| 按钮 | Button | 触发添加对比操作 |
| 列表容器 | List | 展示对比历史记录 |
| 列表项 | ListItem | 对应单条对比记录 |
| 循环渲染 | ForEach | 遍历渲染对比列表 |
| 空白填充 | Blank | 自动填充中间空间 |
| 文本显示 | Text | 显示商品信息和价格 |
2.3 数据模型设计
价格对比应用的数据模型设计围绕商品对比场景展开:
@State app_products: Record<string, string>[] = [];
@State app_productName: string = '';
@State app_price1: string = '';
@State app_price2: string = '';
| 状态变量 | 类型 | 初始值 | 说明 |
|---|---|---|---|
| app_products | Record<string, string>[] | [] | 商品对比记录数组 |
| app_productName | string | ‘’ | 用户输入的商品名称 |
| app_price1 | string | ‘’ | 店铺A的价格输入值 |
| app_price2 | string | ‘’ | 店铺B的价格输入值 |
商品对比记录的数据结构采用Record类型来定义:
Record<string, string> = {
'name': '商品名称', // string
'price1': '店铺A价格', // string
'price2': '店铺B价格', // string
'cheaper': 'A' 或 'B' // string,表示更便宜的店铺
}
使用Record类型而非自定义接口是ArkTS开发中的常见做法。Record<string, string>表示键为string类型、值为string类型的对象类型,这种设计适合结构相对简单且字段类型统一的业务场景。
3. 核心代码详解
3.1 状态管理机制
价格对比应用的状态管理采用了多状态变量协同的模式:
@State app_products: Record<string, string>[] = [];
@State app_productName: string = '';
@State app_price1: string = '';
@State app_price2: string = '';
四个状态变量的职责划分:
app_products:核心数据状态,存储所有商品的价格对比记录数组,每添加一条对比就push一个元素进去
app_productName:商品名称的临时输入状态,绑定到商品名称输入框,实时反映用户的输入内容
app_price1:店铺A价格的临时输入状态,绑定到店铺A价格输入框
app_price2:店铺B价格的临时输入状态,绑定到店铺B价格输入框
状态管理的关键点在于分离"临时输入状态"和"持久数据状态"。商品名称和价格输入框的值先存储在临时状态中,只有用户点击"添加对比"按钮后,才会将临时状态的数据组装成完整的记录对象并添加到持久数据数组中。
3.2 添加对比功能
添加对比功能是应用的核心业务逻辑:
Button('添加对比')
.width('100%')
.onClick(() => {
let app_product: Record<string, string> = {
'name': this.app_productName,
'price1': this.app_price1,
'price2': this.app_price2,
'cheaper': this.app_getCheaper()
};
this.app_products.push(app_product);
})
添加对比的完整流程:
- 用户在商品名称输入框输入商品名称,触发onChange事件更新app_productName
- 用户在店铺A价格输入框输入价格,触发onChange事件更新app_price1
- 用户在店铺B价格输入框输入价格,触发onChange事件更新app_price2
- 用户点击"添加对比"按钮,触发onClick事件执行添加逻辑
- 创建一个包含名称、两个价格和更便宜店铺的Record对象
- 调用app_getCheaper方法计算哪个店铺更便宜
- 使用push方法将新记录添加到app_products数组
- ArkUI自动检测到app_products数组的变化,触发UI重新渲染
价格比较逻辑的实现:
private app_getCheaper(): string {
let app_p1: number = parseFloat(this.app_price1 || '0');
let app_p2: number = parseFloat(this.app_price2 || '0');
return app_p1 <= app_p2 ? 'A' : 'B';
}
这里使用了parseFloat将字符串价格转换为数值进行比较。如果店铺A价格小于等于店铺B价格,返回’A’表示店铺A更便宜;否则返回’B’表示店铺B更便宜。使用|| '0’是为了处理空字符串的情况,当输入框为空时默认按0处理。
3.3 列表渲染机制
ForEach组件是ArkUI中渲染列表的核心组件,价格对比应用使用它来展示对比历史:
List() {
ForEach(this.app_products, (app_product: Record<string, string>) => {
ListItem() {
Column({ space: 8 }) {
Text(app_product['name'])
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.app_color_text_primary'))
Row() {
Column() {
Text('店铺A')
.fontSize(12)
.fontColor($r('app.color.app_color_text_tertiary'))
Text(`¥${app_product['price1']}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(app_product['cheaper'] === 'A' ? $r('app.color.app_color_success') : $r('app.color.app_color_text_secondary'))
}
.layoutWeight(1)
Column() {
Text('店铺B')
.fontSize(12)
.fontColor($r('app.color.app_color_text_tertiary'))
Text(`¥${app_product['price2']}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(app_product['cheaper'] === 'B' ? $r('app.color.app_color_success') : $r('app.color.app_color_text_secondary'))
}
.layoutWeight(1)
}
}
.width('100%')
.padding(12)
.backgroundColor($r('app.color.app_color_white'))
.borderRadius(8)
}
})
}
.width('100%')
.layoutWeight(1)
ForEach的关键参数说明:
| 参数 | 说明 |
|---|---|
| this.app_products | 要遍历的商品对比记录数组 |
| app_product | 当前遍历到的商品记录对象 |
ForEach的渲染机制特点:
- 数组中每个元素渲染一个ListItem
- 每个ListItem内部包含商品名称显示和两个店铺价格对比区域
- 数组变化时自动重新渲染列表,开发者无需手动管理列表更新
3.4 条件渲染与价格高亮
价格对比应用的一个重要特性是根据比较结果高亮显示更便宜的店铺。这通过条件渲染实现:
Text(`¥${app_product['price1']}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(app_product['cheaper'] === 'A' ? $r('app.color.app_color_success') : $r('app.color.app_color_text_secondary'))
条件渲染的逻辑解析:
| 条件 | 价格A的显示颜色 | 价格B的显示颜色 |
|---|---|---|
| cheaper === ‘A’ | app_color_success(绿色) | app_color_text_secondary(灰色) |
| cheaper === ‘B’ | app_color_text_secondary(灰色) | app_color_success(绿色) |
这种视觉设计让用户可以一目了然地看出哪个店铺的价格更低。绿色的价格表示更实惠,灰色的价格表示相对较贵。
3.5 输入处理机制
TextInput组件的onChange事件处理用户输入:
TextInput({ placeholder: '商品名称' })
.width('100%')
.onChange((app_value: string) => {
this.app_productName = app_value;
})
onChange回调的参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| app_value | string | 用户输入的文本内容 |
输入处理的关键点:
- onChange在用户每次输入变化时触发,而非仅在输入结束时触发
- 将输入值直接赋值给@State状态变量,触发UI自动更新
- ArkTS不支持解构赋值,所以使用临时变量接收参数后再赋值
价格输入框使用layoutWeight实现左右对称布局:
Row({ space: 12 }) {
TextInput({ placeholder: '店铺A价格' })
.layoutWeight(1)
.onChange((app_value: string) => {
this.app_price1 = app_value;
})
Text('VS')
.fontColor($r('app.color.app_color_text_tertiary'))
TextInput({ placeholder: '店铺B价格' })
.layoutWeight(1)
.onChange((app_value: string) => {
this.app_price2 = app_value;
})
}
layoutWeight(1)让两个输入框平分父容器的剩余空间,实现自适应的左右对称效果。VS文字作为视觉分隔符,帮助用户理解这是两个价格的对标关系。
4. UI布局设计
4.1 整体布局结构
价格对比应用的UI布局采用垂直堆叠的Column容器:
build() {
Column() {
CommonTitleBar({
app_title: '价格对比',
app_showBack: true
})
Column({ space: 16 }) {
// 商品名称输入
// 价格对比输入行
// 添加按钮
// 对比历史列表
}
.width('100%')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.app_color_background'))
}
布局层次说明:
- 最外层Column充满整个屏幕,背景色为app_color_background
- CommonTitleBar提供导航功能
- 内层Column包含所有功能区块,间距为16vp
- 内层Column左右padding为16vp
4.2 输入区域设计
输入区域包含商品名称输入框和价格对比输入行:
TextInput({ placeholder: '商品名称' })
.width('100%')
.onChange((app_value: string) => {
this.app_productName = app_value;
})
设计要点:
- width(‘100%’)让输入框占满父容器宽度
- placeholder设置无输入时的提示文字
- onChange实时同步输入内容到状态变量
价格对比输入行采用Row布局实现左右对称:
Row({ space: 12 }) {
TextInput({ placeholder: '店铺A价格' })
.layoutWeight(1)
.onChange((app_value: string) => {
this.app_price1 = app_value;
})
Text('VS')
.fontColor($r('app.color.app_color_text_tertiary'))
TextInput({ placeholder: '店铺B价格' })
.layoutWeight(1)
.onChange((app_value: string) => {
this.app_price2 = app_value;
})
}
设计要点解析:
- Row容器内三个子元素水平排列
- 两个TextInput各使用layoutWeight(1)平分空间
- Text(‘VS’)作为视觉分隔,不参与layoutWeight分配
- space: 12设置元素之间的间距
4.3 按钮设计
添加对比按钮是用户触发核心操作的入口:
Button('添加对比')
.width('100%')
.onClick(() => {
// 添加逻辑
})
按钮样式采用默认样式(圆角胶囊形),width(‘100%’)让按钮与上方输入框对齐。onClick事件处理器执行添加对比的完整逻辑。
4.4 列表卡片设计
对比历史列表的每一项都是一个独立的卡片:
Column({ space: 8 }) {
// 商品名称
Row() {
// 店铺A价格
Column() {...}
// 店铺B价格
Column() {...}
}
}
.width('100%')
.padding(12)
.backgroundColor($r('app.color.app_color_white'))
.borderRadius(8)
卡片设计要点:
- 白色背景与页面背景形成对比
- borderRadius(8)设置8vp的圆角
- padding(12)设置内边距
- Column内部space: 8控制元素垂直间距
5. ArkTS核心特性应用
5.1 Record类型使用
价格对比应用使用Record类型定义商品对比记录:
@State app_products: Record<string, string>[] = [];
Record类型的特点:
| 特性 | 说明 |
|---|---|
| 键类型 | 固定为string |
| 值类型 | 固定为string |
| 适用场景 | 简单键值对结构 |
| 灵活性 | 适合字段类型统一的业务数据 |
Record类型的优势在于无需预先定义接口,可以直接在代码中使用。这种方式在ArkTS开发中广泛使用,特别适合数据结构相对简单且字段类型统一的场景。
5.2 ForEach组件详解
ForEach是ArkUI声明式渲染的核心组件:
ForEach(this.app_products, (app_product: Record<string, string>) => {
ListItem() { ... }
})
ForEach的渲染流程:
- ForEach接收app_products数组作为数据源
- 遍历数组的每个元素
- 对每个元素执行回调函数生成UI组件
- 返回的UI组件被插入到渲染树中
ForEach的特点:
- 数据驱动:数组变化自动触发重新渲染
- 键值追踪:通过索引追踪每个元素
- 高效更新:只更新变化的元素
5.3 条件渲染实现
价格对比应用大量使用三元运算符实现条件渲染:
.fontColor(app_product['cheaper'] === 'A' ? $r('app.color.app_color_success') : $r('app.color.app_color_text_secondary'))
条件渲染的三元表达式结构:
condition ? value_if_true : value_if_false
这种模式在ArkUI中广泛使用,用于根据状态动态决定UI属性值。
5.4 layoutWeight布局权重
layoutWeight是ArkUI中实现自适应布局的重要属性:
Column() {
Column() {...}.layoutWeight(1)
Column() {...}.layoutWeight(1)
}
layoutWeight的工作原理:
- 父容器计算剩余空间(总宽度减去固定宽度元素和间距)
- 所有带layoutWeight的子元素按权重比例分配剩余空间
- 两个layoutWeight(1)表示平均分配
6. 购物比价应用开发要点
6.1 表单输入处理
购物比价类应用的表单输入处理有几个关键点:
实时同步:使用onChange事件实时同步输入值到状态变量,而非等到表单提交:
TextInput({...})
.onChange((app_value: string) => {
this.app_xxx = app_value;
})
类型转换:价格输入是字符串,需要转换为数值进行比较:
let app_p1: number = parseFloat(this.app_price1 || '0');
空值处理:使用||操作符提供默认值:
this.app_price1 || '0' // 空字符串时使用'0'
6.2 数组状态管理
ArkTS中数组状态管理的正确方式:
添加元素:使用push方法:
this.app_products.push(newProduct);
删除元素:使用splice方法:
this.app_products.splice(index, 1);
修改元素:直接通过索引修改:
this.app_products[index]['field'] = newValue;
注意事项:
- push和splice直接修改原数组,自动触发UI更新
- ArkTS不支持解构赋值和箭头函数的filter/map等高阶函数
- 建议使用传统的for循环进行数据过滤
6.3 列表性能优化
列表性能优化的常用方法:
保持Item简单:避免在Item中执行复杂计算
正确使用layoutWeight:确保列表容器正确设置layoutWeight
减少不必要的重新渲染:数组引用变化时才触发重新渲染
// 正确做法:直接修改数组
this.app_products.push(item);
// 需要替换整个数组时
this.app_products = [...this.app_products, newItem]; // 这种方式可能触发整个列表重新渲染
7. 扩展与展望
7.1 当前功能总结
价格对比应用实现了以下核心功能:
| 功能模块 | 实现描述 |
|---|---|
| 商品名称输入 | 通过TextInput组件输入商品名称 |
| 价格对比输入 | 同时输入两个店铺的价格 |
| 价格比较逻辑 | 自动计算哪个店铺更便宜 |
| 对比结果展示 | 使用List和ForEach渲染历史记录 |
| 价格高亮显示 | 更便宜的店铺价格用绿色显示 |
7.2 功能扩展方向
基于当前应用架构,可以进行以下功能扩展:
数据持久化:使用AppStorage保存对比记录:
aboutToDisappear(): void {
app_setString('price_compare_records', JSON.stringify(this.app_products));
}
aboutToAppear(): void {
const app_stored: string = app_getString('price_compare_records', '[]');
try {
this.app_products = JSON.parse(app_stored);
} catch (error) {
console.error('加载对比记录失败');
}
}
删除对比记录:支持删除单条对比记录:
Button('删除')
.onClick(() => {
this.app_products.splice(app_index, 1);
})
编辑对比记录:支持修改已有的对比记录:
Button('编辑')
.onClick(() => {
// 显示编辑对话框
// 更新对应记录
})
支持更多店铺:从2个店铺扩展到多个店铺:
@State app_prices: Record<string, string>[] = [];
interface ProductRecord {
name: string;
prices: Record<string, number>; // 店铺名 -> 价格
cheapest: string; // 最便宜的店铺
}
价格走势:记录同一商品的价格历史:
interface PriceHistory {
name: string;
records: Array<{
date: string;
prices: Record<string, number>;
}>;
}
价格提醒:当价格低于设定阈值时提醒用户:
interface PriceAlert {
name: string;
targetPrice: number;
shop: string;
}
二维码扫码:扫描商品二维码自动添加商品:
// 集成扫码功能
Button('扫码添加')
.onClick(() => {
// 启动扫码能力
// 解析商品信息
})
分享对比结果:将对比结果分享给好友:
Button('分享')
.onClick(() => {
// 生成分享内容
// 调用分享能力
})
8. 技术要点总结
8.1 核心代码模块
价格对比应用的代码结构:
@Entry
@Component
struct PriceCompare {
@State app_products: Record<string, string>[] = [];
@State app_productName: string = '';
@State app_price1: string = '';
@State app_price2: string = '';
build() {
Column() {
CommonTitleBar({...})
Column({ space: 16 }) {
TextInput({...}) // 商品名称
Row({ space: 12 }) {
TextInput({...}) // 店铺A价格
Text('VS')
TextInput({...}) // 店铺B价格
}
Button('添加对比'){...}
List() {
ForEach(this.app_products, (app_product) => {...})
}.layoutWeight(1)
}.padding(16)
}.backgroundColor(...)
}
private app_getCheaper(): string {...}
}
8.2 关键技术点
| 技术点 | 在本应用中的体现 |
|---|---|
| @State装饰器 | 管理商品数组和输入状态 |
| TextInput组件 | 输入商品名称和价格 |
| Button组件 | 触发添加对比操作 |
| List组件 | 展示对比历史记录 |
| ForEach组件 | 遍历渲染对比列表 |
| Record类型 | 定义商品记录数据结构 |
| layoutWeight | 实现价格输入框左右对称 |
| 条件渲染 | 根据比较结果高亮显示价格 |
8.3 数组操作方法
| 方法 | 用途 | 示例 |
|---|---|---|
| push | 添加元素到末尾 | this.app_products.push(product) |
9. 完整代码结构
import { CommonTitleBar } from '../../components/CommonTitleBar';
@Entry
@Component
struct PriceCompare {
@State app_products: Record<string, string>[] = [];
@State app_productName: string = '';
@State app_price1: string = '';
@State app_price2: string = '';
build() {
Column() {
CommonTitleBar({
app_title: '价格对比',
app_showBack: true
})
Column({ space: 16 }) {
TextInput({ placeholder: '商品名称' })
.width('100%')
.onChange((app_value: string) => {
this.app_productName = app_value;
})
Row({ space: 12 }) {
TextInput({ placeholder: '店铺A价格' })
.layoutWeight(1)
.onChange((app_value: string) => {
this.app_price1 = app_value;
})
Text('VS')
.fontColor($r('app.color.app_color_text_tertiary'))
TextInput({ placeholder: '店铺B价格' })
.layoutWeight(1)
.onChange((app_value: string) => {
this.app_price2 = app_value;
})
}
Button('添加对比')
.width('100%')
.onClick(() => {
let app_product: Record<string, string> = {
'name': this.app_productName,
'price1': this.app_price1,
'price2': this.app_price2,
'cheaper': this.app_getCheaper()
};
this.app_products.push(app_product);
})
List() {
ForEach(this.app_products, (app_product: Record<string, string>) => {
ListItem() {
Column({ space: 8 }) {
Text(app_product['name'])
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.app_color_text_primary'))
Row() {
Column() {
Text('店铺A')
.fontSize(12)
.fontColor($r('app.color.app_color_text_tertiary'))
Text(`¥${app_product['price1']}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(app_product['cheaper'] === 'A' ? $r('app.color.app_color_success') : $r('app.color.app_color_text_secondary'))
}
.layoutWeight(1)
Column() {
Text('店铺B')
.fontSize(12)
.fontColor($r('app.color.app_color_text_tertiary'))
Text(`¥${app_product['price2']}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(app_product['cheaper'] === 'B' ? $r('app.color.app_color_success') : $r('app.color.app_color_text_secondary'))
}
.layoutWeight(1)
}
}
.width('100%')
.padding(12)
.backgroundColor($r('app.color.app_color_white'))
.borderRadius(8)
}
})
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.app_color_background'))
}
private app_getCheaper(): string {
let app_p1: number = parseFloat(this.app_price1 || '0');
let app_p2: number = parseFloat(this.app_price2 || '0');
return app_p1 <= app_p2 ? 'A' : 'B';
}
}
10. 结语
价格对比应用作为一款购物辅助工具,展示了HarmonyOS ArkUI框架在表单处理和列表展示方面的核心能力。通过TextInput组件实现用户输入,通过List和ForEach组件实现对比历史记录展示,通过条件渲染实现价格高亮效果。
从技术角度来看,应用的核心亮点在于表单输入处理和价格比较逻辑。TextInput的onChange事件实现实时输入同步,parseFloat函数实现字符串到数值的转换,三元运算符实现条件渲染和价格高亮。
从应用设计的角度来看,价格对比应用遵循了"简洁直观、操作便捷"的设计原则。用户只需输入商品名称和两个店铺的价格,系统自动计算并高亮显示更便宜的选项。列表卡片设计让历史对比记录一目了然。
更多推荐




所有评论(0)