HarmonyOS 元服务基础功能开发(三)
1. 学习目标
完成本章学习后,你应当能够:
2. 使用 Image、Text 和 TextInput 构建农业设备界面。
3. 为组件设置常用属性,处理图片显示、文字样式和输入类型。
4. 使用 onClick 等事件完成按钮交互。
5. 理解页面路由与交互流程之间的关系。
6. 区分关系型数据库 RelationalStore 和偏好型数据库 Preferences。
7. 根据数据复杂度选择合适的本地存储方式。
8. 设计农业设备本地数据表和键值配置。
9. 使用事务、查询、插入、更新和删除等基本数据库操作思路。
10. 使用 @State 等状态机制驱动 UI 数据刷新。
11. 处理加载、空数据、错误和保存成功等界面状态。
12. 对比“重新查询刷新”“局部状态更新”“定时刷新”“事件触发刷新”等策略。
13. 编写简短技术分析报告,说明实践中的问题、取舍与收获。
14. 在数据安全、权限、设备控制和工程伦理方面保持责任意识。
2.图片组件 Image
2.1 组件作用
Image 用于显示本地图片、资源图片或符合要求的网络图片。常见用途:
• 元服务卡片图标;
• 农业设备类型图;
• 状态提示图;
• 空数据插图;
• 用户头像或设备照片;
• 背景和装饰图片。
基本示例:
Image($r('app.media.device_pump'))
.width(64)
.height(64)
资源引用的具体形式应以当前 DevEco Studio 工程模板为准。
2.2 图片来源
本地资源
本地资源适合:
• 应用图标;
• 固定的设备类型图;
• 空状态插图;
• 不需要联网的页面元素。
优点:
• 加载稳定;
• 不依赖网络;
• 响应速度快;
• 便于版本控制。
网络图片
网络图片适合:
• 设备现场照片;
• 远程图片;
• 动态更新的宣传图或缩略图。
使用网络图片时要考虑:
• 网络失败;
• 加载时间;
• 图片尺寸过大;
• 缓存;
• 访问权限;
• 用户隐私;
• 图片地址是否可信;
• 是否需要占位图和错误图。
网络图片不应直接被视为可信内容。来源不明的图片地址可能导致隐私泄露、恶意跳转或不必要的流量消耗。
2.3 图片显示模式
图片原始比例与容器比例不一致时,需要选择合适的显示策略。常见思路:

实际 API 名称和属性会随 SDK 版本变化,学习时重点掌握“保持比例、裁剪、填充”的设计原则。
2.4 图片加载状态
网络图片至少应考虑:
加载中 → 成功显示
└→ 失败显示占位图或错误提示
概念示例:
type ImageState =
| { kind: 'loading' }
| { kind: 'success'; source: string }
| { kind: 'error' };
@Component
struct DeviceImage {
@Prop state: ImageState;
build() {
if (this.state.kind === 'loading') {
Text('图片加载中')
} else if (this.state.kind === 'success') {
Image(this.state.source)
} else {
Image($r('app.media.image_placeholder'))
}
}
}
对于卡片,应优先保证核心数据可读,图片加载失败不应导致整个卡片无法使用。
2.5 图片可用性与性能
• 使用适合显示尺寸的图片,避免把超大原图直接放入小卡片。
• 对网络图片设置缓存策略。
• 重要图片准备占位图。
• 图片无法加载时提供可理解的反馈。
• 图片旁边配合文字,不能只依靠颜色或图像传达关键状态。
• 设备照片涉及人员、地块和位置时,要遵守隐私和数据使用规定。
3.文本组件 Text
3.1 组件作用
Text 用于显示字符串内容,是界面中最基础的信息组件。常见用途:
• 页面标题;
• 设备名称;
• 状态文本;
• 数量和单位;
• 时间;
• 错误提示;
• 操作说明。
基本示例:
Text('智慧农场')
.fontSize(20)
.fontWeight(FontWeight.Bold)
3.2 常用文本属性
Text('在线设备:18')
.fontSize(16)
.fontColor('#087443')
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
常见属性包括:

具体属性名称以当前 SDK 的类型提示为准。
3.3 文本层级设计
农业设备卡片可以采用:
一级信息:智慧农场
二级信息:在线设备数量
三级信息:最近更新时间
辅助信息:设备位置、说明文字
示例:
Column({ space: 4 }) {
Text('东区智能灌溉泵')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('东区一号田')
.fontSize(13)
.opacity(0.65)
Text('最近同步:08:30')
.fontSize(12)
.opacity(0.55)
}
不要让所有文字都使用同样的字号、颜色和粗细,否则用户很难判断重点。
3.4 长文本与异常数据
设备名称、位置和错误消息可能很长,应进行限制:
function displayName(name: string): string {
const maxLength = 18;
return name.length <= maxLength
? name
: `${name.slice(0, maxLength - 1)}…`;
}
对外部数据还要考虑:
• 空字符串;
• null;
• 未知状态;
• 超大数字;
• 非法日期;
• 包含特殊字符的设备名称。
界面显示应使用安全的文本渲染方式,不要把外部输入当作可执行内容。
4.输入组件 TextInput
4.1 组件作用
TextInput 提供文本输入框功能,常用于:
• 搜索设备;
• 输入设备名称;
• 输入位置;
• 输入备注;
• 填写账号;
• 输入密码;
• 输入数字或编号。
示例:
@Component
struct EquipmentSearch {
@State keyword: string = '';
build() {
TextInput({
placeholder: '请输入设备名称或位置',
text: this.keyword
})
.onChange((value: string) => {
this.keyword = value;
})
.width('100%')
}
}
具体参数名可能因 SDK 版本不同而变化,应参考 IDE 的类型提示。
4.2 输入类型
常见输入类型:
• 普通文本;
• 密码;
• 数字;
• 电话号码;
• 邮箱;
• 多行文本。
输入类型的选择应与业务匹配。例如:
设备名称 → 文本
设备编号 → 文本或数字,取决于编号规则
电量百分比 → 数字
登录密码 → 密码
设备备注 → 多行文本
不要把所有输入框都设置成普通文本后再依赖字符串处理,合理的输入类型可以改善键盘、校验和用户体验。
4.3 受控输入与状态
将输入值绑定到状态:
@State keyword: string = '';
输入变化时更新:
.onChange((value: string) => {
this.keyword = value.trimStart();
})
界面使用:
Text(`当前搜索:${this.keyword}`)
数据流:
用户输入
↓
onChange 事件
↓
更新 @State
↓
绑定的 Text 或列表重新显示
4.4 输入校验
设备名称校验
function validateName(name: string): string | null {
const value = name.trim();
if (value.length === 0) {
return '设备名称不能为空';
}
if (value.length > 50) {
return '设备名称不能超过 50 个字符';
}
return null;
}
电量校验
function parseBattery(input: string): number | null {
const value = Number(input);
if (!Number.isInteger(value) || value < 0 || value > 100) {
return null;
}
return value;
}
输入框的校验应在提交前再次执行,不能只依赖 UI 控件的输入类型。
4.5 防抖搜索
搜索输入每变化一次都发起请求,可能造成大量请求:
输 入 “ 水 泵 ”
↑ ↑ ↑
多次触发网络或数据库查询
可以采用防抖思路:
用户停止输入一小段时间
↓
只执行最后一次搜索
课程练习可以先实现本地过滤,再研究定时器和防抖。对于本地数组,数据量不大时不必过度复杂化。
4.6 输入安全
• 限制输入长度;
• 对设备名称和备注进行合法性检查;
• 不在日志中打印密码;
• 不把密码写入普通偏好配置;
• 外部输入显示时进行安全处理;
• 对数字输入检查范围和小数位;
• 对真实设备编号进行格式校验。
5.点击事件 onClick
5.1 基本写法
Button('刷新')
.onClick(() => {
this.refreshData();
})
也可以将事件处理函数单独定义:
@Component
struct RefreshControl {
@State loading: boolean = false;
private handleRefresh = (): void => {
if (this.loading) {
return;
}
void this.refreshData();
};
private async refreshData(): Promise<void> {
this.loading = true;
try {
// 读取数据
} finally {
this.loading = false;
}
}
build() {
Button(this.loading ? '刷新中' : '刷新')
.onClick(this.handleRefresh)
}
}
5.2 点击事件的完整流程
一个可靠的点击操作通常包含:
用户点击
↓
检查当前状态
├── 不允许操作 → 提示原因
└── 允许操作
↓
显示处理中
↓
执行同步/数据库操作
↓
成功 → 更新状态并反馈
失败 → 显示错误并允许重试
5.3 防止重复点击
@State saving: boolean = false;
private async saveEquipment(): Promise<void> {
if (this.saving) {
return;
}
this.saving = true;
try {
await insertEquipment();
} catch (error: unknown) {
console.error(`保存失败:${String(error)}`);
} finally {
this.saving = false;
}
}
需要注意:前端防重复点击只是第一道保护。服务层也应考虑幂等性和重复请求。
5.4 不同按钮的责任

涉及删除和远程控制的按钮,需要比普通查看按钮更严格的确认和权限检查。
6.页面路由与交互流程
本章培养能力中包含页面路由。即使当前重点是基础组件,也应理解组件交互与页面跳转的关系。
6.1 为什么需要路由
当页面内容变复杂时,通常会拆成:
设备首页
├── 设备列表
├── 设备详情
├── 新增设备
└── 设置页面
路由负责在这些页面之间切换,并传递必要参数。
6.2 路由参数
进入详情页时通常需要传递设备 ID:
设备列表 → 设备详情
└── equipmentId = 1001
建议传递稳定的业务 ID,而不是完整对象:
推荐:传 equipmentId
谨慎:传整个设备对象
原因:
• 详情页可以重新读取最新数据;
• 避免列表对象与详情对象不同步;
• 参数更小;
• 页面恢复时更可靠。
6.3 路由错误处理
点击详情时可能出现:
• 设备已删除;
• ID 无效;
• 本地数据库读取失败;
• 页面不存在;
• 权限不足。
详情页应准备:
加载中
设备存在 → 显示详情
设备不存在 → 显示“设备不存在”
读取失败 → 显示错误和重试
7.关系型数据库 RelationalStore
7.1 适用场景
关系型数据库适合保存结构化、数量较多、需要查询和关联的数据,例如:
• 农业设备列表;
• 设备状态历史;
• 传感器采样记录;
• 告警记录;
• 操作日志;
• 用户与设备的关系;
• 分页、排序和条件筛选数据。
课程材料将 RelationalStore 与 SQLite 结构化存储联系起来。不同系统版本的 API 初始化方式、配置字段和异步接口可能不同,实际编码应以当前 HarmonyOS SDK 文档和 IDE 模板为准。
7.2 数据库基本概念
- 数据库
存放一个应用或模块的数据集合。 - 表
按照主题组织数据,例如:
equipment:设备表
equipment_log:设备日志表
alarm:告警表
- 行
表中的一条记录,对应一台设备或一条日志。 - 列
记录的字段,例如:
id、name、status、location、battery、updated_at
- 主键
唯一标识一行数据,设备表中的 id 通常可以作为主键。 - 索引
帮助提高查询速度,但索引越多,写入和存储开销越大。
7.3 农业设备表设计
示例字段:

设计原则:
• 主键必须唯一;
• 状态和类别值要有约束;
• 电量应限制在合理范围;
• 时间格式统一;
• 不要把多个无关值拼在一个字段中;
• 设备详情和日志记录需要时可以拆成不同表。
7.4 建表语句示意
CREATE TABLE equipment (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
status TEXT NOT NULL,
location TEXT NOT NULL,
battery INTEGER NOT NULL,
updated_at TEXT NOT NULL,
created_at TEXT NOT NULL
);
说明:这是数据库设计示意。实际 RelationalStore 的建库、建表和执行 SQL API 需要按照当前 SDK 的接口完成。
7.5 CRUD 操作
CRUD 是数据库最基本的四类操作:

- 新增
INSERT INTO equipment
(id, name, category, status, location, battery, updated_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
不要把用户输入直接拼接到 SQL 字符串中,应使用参数绑定,避免 SQL 注入和引号处理错误。
2. 查询
SELECT id, name, category, status, location, battery, updated_at
FROM equipment
ORDER BY updated_at DESC;
- 条件查询
SELECT *
FROM equipment
WHERE status = ? AND name LIKE ?;
- 更新
UPDATE equipment
SET status = ?, battery = ?, updated_at = ?
WHERE id = ?;
- 删除
DELETE FROM equipment
WHERE id = ?;
删除前应进行确认,并根据业务需要记录删除操作日志。
7.6 事务
事务用于保证一组数据库操作“要么全部成功,要么全部失败”。
例如新增设备时,同时写入设备表和初始化日志表:
开始事务
↓
写入 equipment
↓
写入 equipment_log
├── 全部成功 → 提交事务
└── 任一步失败 → 回滚事务
适合使用事务的场景:
• 多表写入;
• 批量新增或更新;
• 设备状态和日志必须同步;
• 删除设备及相关数据;
• 导入一批设备数据。
事务并不是越多越好。只把必须保持一致的一组操作放在同一事务中,事务时间过长会影响并发和性能。
7.7 查询结果与类型转换
数据库返回的数据需要转换为业务对象:
interface Equipment {
id: string;
name: string;
category: EquipmentCategory;
status: EquipmentStatus;
location: string;
battery: number;
updatedAt: string;
}
转换时检查:
• 字段是否存在;
• 数字是否真的为数字;
• 状态是否属于允许集合;
• 时间是否可以解析;
• 空值是否符合业务规则。
不要认为“数据库字段有类型”就可以跳过应用层检查。
8.偏好型数据库 Preferences
8.1 适用场景
Preferences 以键值对形式保存轻量配置,适合:
• 用户选择的主题;
• 上次选择的筛选条件;
• 是否显示引导页;
• 最近一次打开的页面;
• 卡片刷新偏好;
• 简单的用户设置;
• 小规模、结构简单的本地配置。
示意:
"theme" → "dark"
"pageSize" → 20
"showOfflineOnly" → true
"lastLocation" → "东区一号田"
8.2 不适合保存什么
不建议使用 Preferences 保存:
• 大量设备记录;
• 需要复杂条件查询的数据;
• 设备状态历史;
• 多表关联数据;
• 大量图片或二进制内容;
• 需要事务保证的数据;
• 明文密码和敏感凭证。
8.3 读写思路
概念示例:
interface UserPreferences {
theme: 'light' | 'dark';
pageSize: number;
lastStatus: EquipmentStatus | 'all';
}
保存:
const preferences: UserPreferences = {
theme: 'dark',
pageSize: 20,
lastStatus: 'offline'
};
// 使用当前 SDK 的 Preferences API 保存键值
读取:
const theme = 'light'; // 读取不到时使用默认值
实际 API 名称、实例获取方式和异步调用方式应以当前 SDK 文档为准。学习重点是“按 key 保存小型配置,读取时提供默认值”。
8.4 默认值策略
配置读取失败或 key 不存在时,不应让页面直接崩溃:
function normalizePageSize(value: unknown): number {
if (
typeof value !== 'number' ||
!Number.isInteger(value) ||
value < 1 ||
value > 100
) {
return 20;
}
return value;
}
默认值应:
• 符合业务规则;
• 对用户可理解;
• 不造成高风险操作;
• 与页面初始状态一致。
8.5 Preferences 安全注意事项
• 不要保存明文密码。
• 不要把访问令牌当普通偏好项处理。
• 不要在日志中打印全部键值。
• 需要敏感数据时使用符合系统要求的安全存储方案。
• 用户退出账号后清理与账号相关的设置。
• 版本升级时考虑配置迁移。
9.RelationalStore 与 Preferences 对比

选择原则:
需要列表查询、条件筛选、排序或多条记录
→ RelationalStore
只是保存几个简单配置项
→ Preferences
两者可以同时使用:
RelationalStore:保存设备和日志
Preferences:保存用户上次使用的筛选条件
10.UI 数据刷新机制
10.1 为什么需要刷新
数据发生变化后,界面必须同步反映新结果。例如:
数据库中的设备状态由 offline 改为 online
↓
页面仍显示 offline
↓
用户看到过期信息
因此需要建立:
数据源变化 → 更新状态 → UI 重新显示
10.2 使用 @State 驱动刷新
@Component
struct EquipmentListPage {
@State items: Equipment[] = [];
@State loading: boolean = false;
@State errorMessage: string = '';
build() {
Column() {
if (this.loading) {
Text('正在加载')
} else if (this.errorMessage.length > 0) {
Text(this.errorMessage)
} else {
ForEach(
this.items,
(item: Equipment) => {
Text(item.name)
},
(item: Equipment) => item.id
)
}
}
}
}
当执行:
this.items = newItems;
绑定 items 的 UI 会根据新数据刷新。
10.3 不要只修改数据库而不更新界面
错误流程:
点击保存
↓
写入数据库
↓
结束
此时数据库是新数据,页面可能仍然是旧数据。
更完整的流程:
点击保存
↓
校验输入
↓
写入数据库
↓
重新查询或更新内存状态
↓
更新 @State
↓
显示成功反馈
10.4 重新查询策略
保存后重新查询:
private async saveAndReload(): Promise<void> {
await saveToDatabase();
const latestItems = await queryEquipments();
this.items = latestItems;
}
优点:
• 页面与数据库最终结果一致;
• 适合复杂触发器、默认值和多表变更;
• 不容易漏掉服务端或数据库自动修改的字段。
缺点:
• 多一次查询;
• 数据量大时成本更高;
• 需要处理查询失败。
10.5 局部状态更新策略
保存成功后直接修改状态:
private updateLocalItem(
id: string,
status: EquipmentStatus
): void {
this.items = this.items.map(item =>
item.id === id
? { ...item, status }
: item
);
}
优点:
• 响应快;
• 减少查询;
• 适合简单、明确的局部变化。
缺点:
• 如果数据库还有其他自动变化,页面可能不完整;
• 多个页面之间可能不同步;
• 需要确保本地更新逻辑与持久化逻辑一致。
10.6 刷新策略对比

本章要求学生对这些策略进行比较,并结合智慧农业场景说明选择理由。
10.7 加载、空数据与错误状态
type ViewState<T> =
| { kind: 'loading' }
| { kind: 'success'; data: T }
| { kind: 'empty'; message: string }
| { kind: 'error'; message: string };
渲染时:
function getViewMessage<T>(state: ViewState<T>): string {
switch (state.kind) {
case 'loading':
return '正在加载设备数据……';
case 'success':
return '数据加载成功';
case 'empty':
return state.message;
case 'error':
return state.message;
}
}
页面应区分:
• “正在加载”;
• “加载成功但没有数据”;
• “筛选后没有结果”;
• “请求失败”;
• “保存失败”;
• “权限不足”。
这些状态不能都显示成“暂无数据”,否则用户无法判断下一步。
更多推荐


所有评论(0)