HarmonyOS ArkTS实战:智慧农业地块管理系统的数据模型与列表页构建

发布时间:2026/10/16 22:06:54
HarmonyOS ArkTS实战:智慧农业地块管理系统的数据模型与列表页构建 这个系列写到第6篇了前面几篇把工程骨架、登录流程、首页仪表盘这些基础内容摸了一遍今天开始进入真正意义上的第一个业务模块——地块管理系统。做过农业类应用开发的兄弟应该能理解地块这个实体在整个智慧农业项目里就是「数据底座」面积、作物、传感器、灌溉任务、告警记录全都挂在它身上。所以这篇先把数据模型和列表页搞定属于整个模块的地基工程地基歪了后面全得返工。这篇内容面向的是已经跑通 HarmonyOS 基础工程的开发者对 ArkTS 语法和 ArkUI 布局有基本了解还没有独立写过业务模块。我会把地块数据模型的设计思路、ArkTS 类定义方式、列表页的组件拆分、数据渲染以及实际开发中容易踩的坑都讲一遍。所有代码都是直接在 DevEco Studio 里验证过的写法你可以照着敲。1. 篇前回顾地块管理在整套应用里的定位先回到业务本身。一个智慧农业应用不管底下接了多少硬件传感器、控制了多少个灌溉阀门最后落到用户界面上用户首先要看到的一定是「我有哪些地块每块地现在什么状态」。地块列表就是整个应用的流量入口用户从这块点进详情、查看设备、下发控制指令一条业务链全是从这里发起的。所以「高高种地」这个项目里我把地块管理系统设计成了三个递进的层次第一层是地块列表展示概览信息第二层是地块详情展示传感器实时数据、历史趋势第三层是地块操作灌溉控制、告警处理。这篇先做第一层数据模型是贯穿三层的骨架列表页是第一层的外壳。技术选型上我用的是 Stage 模型 ArkTS 语言API 版本选择 HarmonyOS 3.1 以上对应 API 9这个版本对 List 组件的懒加载、ForEach 渲染的稳定性都有保障。如果你用的 API 8语法上差别不大但建议直接上 API 9 之后部分组件行为做了优化后面遇到问题排查起来也简单。页面路由我选择了 Navigation 组件而不是 router 方式。原因有两个一是 Navigation 组件可以省掉在 main_pages.json 里反复注册页面的麻烦二是后续做详情页跳转、返回传参的时候Navigation 的栈管理更直观。如果你已经用 router 把前面的页面都串起来了保持一致也没问题本篇传参方式我会分别说明。2. 数据模型把地块这个业务实体的底子打牢数据模型是整个地块管理系统的核心甚至可以说模型设计占了整个模块 60% 的工作量。因为 ArkTS 对类型的约束比 JavaScript 严格得多字段定义好了页面、组件、交互全都在这个基础上展开字段没定义好后面加一个字段可能牵连四五个文件要改。2.1 字段设计从业务需求倒推我不建议一上来就埋头写代码先拿张纸把业务问题列清楚。一个地块在用户眼里需要展示什么管理者需要关注什么传感器数据要挂接什么。我们按场景来推首先是身份信息地块编号、地块名称、地理位置这是所有地块都有的基础字段。名称用户要直接看到编号用于后台关联设备、做数据链路的唯一标识位置字段将来可以在地图页面上打点。然后是种植信息当前作物种类、生长阶段、负责人。作物种类决定了后续环境参数阈值的判定逻辑比如番茄和叶菜对温度湿度的要求完全不同生长阶段用于推算种植周期也是后面「农事建议」模块的数据来源负责人字段在多人协作的农场场景下必不可少设备告警要推送给对应的人。接着是环境数据土壤湿度、土壤温度、光照强度、更新时间。这四个字段来自传感器地块列表页要展示它们的当前值让用户一进页面就能判断各地块的健康状况。为什么这个字段放在列表里而不是只放在详情里因为列表页的主要价值就是「扫一眼」把异常地块抓出来如果环境数据只放详情页用户得一个个点进去才能看到效率太低。最后是状态字段当前状态正常/预警/离线。这个字段由系统根据阈值计算得出不是用户手动改的但在数据模型里必须有因为列表页要对状态做颜色标识和筛选。至于状态的判定逻辑本篇先不展开放在详情系统那一篇结合传感器历史数据一起讲。字段基本确定下来了需要说明的是我刻意没有放「地理位置经纬度」和「历史数据数组」这两个字段。经纬度目前列表用不上等到做地图模块再扩展历史数据数组应该是独立的「环境记录」模型通过地块编号关联不能塞在单个地块对象里否则内存占用和序列化都会出问题。2.2 状态枚举与字段约束状态字段我用枚举而不是布尔类型或者字符串散值。布尔类型只能表达两种状态不够用字符串散值容易写错比如一处写normal另一处写Normal对不上就出 bug。枚举把取值范围锁死编译期就能发现拼写错误。export enum PlotStatus { Normal normal, Warning warning, Offline offline }等等有人会问为什么枚举值要用字符串而不是默认的数字 0、1、2这里有一个实际考量后端接口返回的数据里地块状态通常是以字符串形式存在的比如status: warning如果枚举值是数字前端每次对接接口都要做一层映射转换多写一堆样板代码。直接用字符串枚举后端返回什么值前端就能直接比较省掉转换。生长阶段目前我用字符串比如苗期开花期结果期。它本质上也是一个有限的取值集合理论上也可以用枚举但生长阶段在不同作物上有差异有的作物还要分成十几个细粒度的阶段枚举会变得非常臃肿。考虑到这个字段在列表页只做展示用、不做逻辑判断字符串的灵活性更好。这里的原则是参与逻辑判断的字段用枚举仅展示的字段用字符串。面积字段我统一用数字类型单位是「亩」。很多项目会在面积字段上直接拼接单位比如存一个2.5亩字符串这个做法在后端做面积汇总计算时会非常痛苦你得先解析字符串再转数字还要处理异常格式。正确的做法是存数字单位在显示层拼接。列表里显示2.5 亩后台计算总面积直接用reduce累加就行。2.3 ArkTS 中类模型的写法与初始化细节模型定义我用 ArkTS 的 class。ArkTS 基于 TypeScript但语法约束更严格有几个细节必须注意否则编译器直接报错。第一个坑类属性必须初始化。在标准 TypeScript 里你可以写name: string然后在构造函数里赋值但 ArkTS 要求所有类属性要么有默认值要么在构造器里明确赋值。更严格的是如果属性没初始化编译时会要求你「添加初始化器」或者「在构造函数中明确分配」。第二个坑构造函数重载有限制。ArkTS 不建议写复杂的构造器重载简单的做法是定义一个不带参数的构造器所有属性取默认值然后单独写一个工厂方法或者静态方法去创建实例。这样做的好处是页面里拿到模型对象时属性一定是完整的不会出现undefined。来看具体代码我定义一个地块数据模型// model/PlotModel.ets export enum PlotStatus { Normal normal, Warning warning, Offline offline } export class PlotModel { id: string name: string area: number 0 crop: string cropStage: string owner: string status: PlotStatus PlotStatus.Normal soilMoisture: number 0 soilTemperature: number 0 lightIntensity: number 0 location: string updateTime: string constructor() { } }这里要重点说下id字段。我用字符串类型而不是数字是因为地块编号往往有业务含义比如PLOT001PLOT002而且对接后端时接口返回的主键经常是字符串形式的雪花 ID。字符串 ID 可以兼容更多场景不用在前后端之间来回转换类型。关于updateTime字段我用字符串存格式化后的时间比如2025-01-12 10:30:00。为什么不存时间戳数字因为列表页要直接展示这个值字符串不需要再做格式化转换。如果某天需要按时间排序字符串格式的时间恰好可以按字典序排列效果等同于时间排序。这个技巧在数据量不大时完全够用。数据模型类写好后我通常还会在同一个文件里加一个「辅助函数」用于从接口返回的原始数据创建模型实例。因为后端接口的字段命名习惯跟前端不一定一致可能字段名是下划线风格或者部分字段缺失。辅助函数把脏数据挡在模型层外面页面组件拿到的永远是干净、完整的对象。export function parsePlotFromJson(json: object): PlotModel { let model new PlotModel() const obj json as Recordstring, object | number | string model.id obj[plot_id] as string ?? model.name obj[name] as string ?? model.area obj[area] as number ?? 0 model.crop obj[crop] as string ?? model.cropStage obj[crop_stage] as string ?? model.owner obj[owner] as string ?? model.status PlotStatus[obj[status] as keyof typeof PlotStatus] ?? PlotStatus.Normal // 环境字段类似不再重复 return model }严格来说ArkTS 里对as转换和??空值合并的支持是有的但有些写法编译器比较挑剔。实际项目中我更推荐的做法是在「本地模拟数据阶段」直接手写PlotModel实例等接入真实接口时再用parsePlotFromJson这类函数。这篇后面用的就是手写实例的方式重点讲清楚模型的形状和用法。3. 列表页面的骨架设计模型定义好了接下来是列表页。ArkUI 的布局方式跟传统前端差异很大它不是 HTML 标签流式布局而是组件树 容器约束的布局思路。第一次接触的开发者容易一股脑把所有内容堆在一个页面里等到要维护的时候发现改一个卡片要动几十行代码。所以先把页面拆分想清楚。3.1 页面结构与组件拆分地块列表页我拆成了四个部分顶部标题栏、搜索输入区、状态筛选标签区、列表内容区。搜索和筛选是列表页的两个操作入口列表是信息展示主体标题栏负责页面级导航。拆分的思路是每个区域对应一个独立的 UI 块区域之间用垂直布局Column堆叠。列表内容区继续往下拆每一行是一个独立的地块卡片组件。父页面只负责管理数据源和交互事件卡片组件只负责渲染单条数据。这样后续维护的时候改卡片的样式不会影响页面其他部分改数据逻辑也不用关心卡片长什么样。ArkUI 里组件化拆分用Component注意子组件接收数据用Prop而不是直接在构造器里传参。Prop装饰器的特点是单向同步父组件数据变化会自动更新子组件但子组件里不能反向修改父组件数据。这个设计对列表场景很合适因为一个卡片不拥有数据只是展示数据。Component export struct PlotCard { Prop plot: PlotModel new PlotModel() build() { // 卡片内容 } }这里需要注意Prop接收对象类型时ArkUI 会做一次深拷贝。对于地块列表这种数据量不会特别大的场景性能没问题。但如果一个页面要同时渲染几百上千条复杂对象就要考虑换成普通成员变量 手动刷新控制这个后续在性能优化篇单独展开。页面外层结构大致是这样Entry Component struct PlotListPage { State plotList: PlotModel[] [] build() { Column() { // 1. 标题栏 // 2. 搜索框 // 3. 筛选标签行 // 4. List 列表 } } }State修饰的plotList是页面级数据源搜索、筛选的结果都通过计算得到并重新赋值给它。所有 UI 的变化追根溯源都是这个数组的变化。3.2 搜索与状态筛选的实现搜索功能我用TextInput组件用户在输入框里输入关键字实时过滤列表。ArkUI 的TextInput提供了onChange回调每次输入内容变化都会触发。关键点在「实时」两个字如果每次输入都去修改整个数据数组性能会有损耗更合理的方式是维护一个搜索关键字状态然后通过一个计算函数从完整数据里过滤。状态筛选也是一样。我定义了一个「当前选中筛选」的状态取值是所有状态all或者某个具体状态normal/warning/offline。筛选条件变化时同样走过滤函数。State searchKey: string State activeFilter: PlotStatus | all all过滤函数的写法getFilteredPlots(): PlotModel[] { let result: PlotModel[] this.plotList if (this.activeFilter ! all) { result result.filter((plot: PlotModel) plot.status this.activeFilter) } let keyword this.searchKey.trim() if (keyword ! ) { result result.filter((plot: PlotModel) { return plot.name.includes(keyword) || plot.crop.includes(keyword) || plot.location.includes(keyword) }) } return result }搜索条件我同时匹配了地块名称、作物种类和位置用户输入番茄能搜到所有种番茄的地块输入东区能搜到东区下的所有地块。这个匹配逻辑是列表页很实用的一点很多新手只做名称搜索等到用户反馈我想按作物找地块才想起来补。筛选标签行我用Row组件横向排列几个标签每个标签是Text组件加边框背景点击时切换选中态。用 ArkUI 的if/else条件渲染来切换不同的样式虽然代码看起来有点繁琐但胜在直观。3.3 地块卡片的布局细节地块卡片是列表项的主体直接决定用户第一眼看到的信息质量。卡片我设计成上下两个区域上方是地块名称、状态标签、地理位置下方是一行环境数据摘要土壤湿度/土壤温度/光照强度。卡片的容器用Column外层加圆角、背景色和阴影。这里有一个细节ArkUI 的阴影.shadow()要加在容器上而且要在背景色设置之后才生效否则阴影可能被背景色覆盖看不见。另外圆角需要搭配clip(true)否则背景色溢出圆角范围。卡片内部的信息行布局用RowSpace实现两端对齐。某个环境指标的展示我用一个小结构上方是数值下方是单位标签。这种「两段式」布局比一行塞下湿度45%要清爽得多而且下面还可以配上一个小图标或者进度条。地块状态的视觉区分正常状态用绿色标签预警状态用橙色标签离线状态用灰色标签。标签本身只是一个Text加圆角背景色但颜色的选择要刻意做区分色盲用户也能通过明暗差异辨认这是很多界面设计里容易忽略的点。卡片的高度不要写死让内容撑起来。ArkUI 里如果给ListItem设置了固定高度内容变化时会出现截断或者空白不如让列表项自适应高度。4. 数据渲染从模拟数据到真实列表页面结构搭好了数据从哪来在还没有后端接口的阶段先造模拟数据把渲染链路跑通。这一步很重要它能验证模型的字段、型别、组件的通信方式是否正常同时也能给后续接入真实接口提供一个清晰的替换点。4.1 模拟数据的构造模拟数据我直接写在页面文件外部的独立数据文件里一般建一个mock/PlotMock.ets。因为地块列表的模拟数据以后很可能也要给详情页复用放独立文件可以避免页面之间互相引用造成循环依赖。// mock/PlotMock.ets import { PlotModel, PlotStatus } from ../model/PlotModel export function getMockPlots(): PlotModel[] { let plots: PlotModel[] [] let plot1 new PlotModel() plot1.id PLOT001 plot1.name 一号西红柿棚 plot1.area 2.5 plot1.crop 番茄 plot1.cropStage 开花期 plot1.owner A管理员 plot1.status PlotStatus.Normal plot1.soilMoisture 45.6 plot1.soilTemperature 22.3 plot1.lightIntensity 32000 plot1.location 东区 plot1.updateTime 2025-01-12 10:30:00 plots.push(plot1) // 再构造几个不同状态的例子 return plots }模拟数据里我会刻意保留几个不同状态的样本一条正常、一条预警比如土壤湿度过低、一条离线传感器无数据。这样列表页的筛选、状态标签、视觉区分这些功能都能一次性验证到位不用调试的时候再去改数据。4.2 ForEach 渲染与 key 值管理列表渲染是 ArkUI 的核心操作。List组件配合ForEach在列表数据到达一定规模比如 50 条以上时List内部的ListItem有懒加载机制不会一次性创建所有组件实例性能比ScrollColumn手工渲染要好得多。List() { ForEach(this.getFilteredPlots(), (plot: PlotModel) { ListItem() { PlotCard({ plot: plot }) } .width(100%) .margin({ bottom: 12 }) }, (plot: PlotModel) plot.id) }ForEach的第三个参数是 key 生成器这个参数很多人会忽略但它决定了列表数据变化时可以复用哪些组件实例。key 值必须满足两个条件在列表内唯一、在元素生命周期内稳定。用plot.id是最合适的因为 id 是数据库层的唯一标识。千万不要用index作为 key当你删除列表中间一条数据时后面的 index 全部变了ArkUI 会认为整个列表都变了导致不必要的重新渲染甚至出现状态错乱。ArkUI 的ForEach对 key 值有一个隐藏要求key 不能发生重复。如果出现两条相同 id 的数据控制台会报 key is duplicated 的警告然后渲染结果不可预期。这个问题在真实项目里出现过原因是后端接口返回的数据有重复记录前端没有做去重就塞进了列表查了半天才发现是数据源的问题。所以我在写列表的aboutToAppear生命周期里会做一个简单的数组去重防御。4.3 点击跳转与传参占位地块卡片不仅是展示点击后应该进入地块详情页。这一篇详情页还没实现先做好跳转的「占位」逻辑保证点击行为有反馈后续实现详情页时直接填内容。使用 Navigation 组件的话页面跳转前要先用Navigation(this.pageStack)包裹页面结构然后通过this.pageStack.pushPath跳转。传参可以直接放一个对象进去。Provide(pageStack) pageStack: NavPathStack new NavPathStack() // 在卡片点击时 this.pageStack.pushPath({ name: PlotDetailPage, param: plot.id })使用 router 方式的话调用router.pushUrl传入参数然后在详情页的onPageShow或构造参数里通过router.getParams()取回。传参这里有一个经验之谈只传 id 或者最小必要信息不要直接把整个PlotModel对象塞给详情页。原因有两点一是PlotModel里的环境字段是动态变化的详情页应该根据 id 拉取最新数据而不是显示列表页当时的旧快照二是路由参数传递复杂对象时序列化/反序列化开销大还容易碰到一些类型被转成普通 Object 后取不到属性的情况。5. 常见问题与排查技巧实录每次写系列教程我都要把实际开发中踩过的坑单独整理出来这些内容在官方文档里往往找不到直接答案但对正在开发的人来说可能比正文更有价值。5.1 列表数据显示了但页面不刷新这是新手遇到最多的问题。现象是数据源plotList数组里的某个对象的属性变了比如状态从正常变成了预警但界面上完全没有反应。原因在于 ArkUI 的状态管理机制State装饰的数组如果直接修改数组中某个对象的属性比如this.plotList[0].status PlotStatus.WarningArkUI 的响应式系统不会感知到这次修改。因为State观察到的是数组引用变化而对象属性的内部修改对它来说是不可见的。解决办法有三个。第一个是整体替换数组创建一个新数组再赋值给plotList能触发刷新但性能一般第二个是给数据模型加Observed装饰器页面里用ObjectLink接收能让对象属性级别的修改被感知第三个最实际——在列表这种展示场景每次数据更新本来就可以认为是整批更新直接把新数据赋给一个新数组就行避免过度设计。// 触发刷新的方式 this.plotList this.plotList.map((item: PlotModel) { if (item.id targetId) { item.status PlotStatus.Warning } return item })map返回新数组引用变化了ArkUI 就能感知到。这个方法代码量少也不破坏数据模型的结构。5.2 ForEach 的 key 值重复导致渲染错乱症状是列表里某些卡片内容重复或者删除一条数据后界面显示的数据跟数据源对不上。排查思路第一站永远是 key 生成器。如果 key 生成器返回的是一个常数那所有列表项都会被认为同一个 itemArkUI 只渲染一个如果返回的是可能重复的值比如所有地块的crop字段就会出现数据错乱。我在项目里就见过有人用plot.crop作为 key 的一旦两条数据都是番茄渲染立刻出问题。用plot.id作为 key 后还要在数据源层面保证 id 不重复。如果数据来自接口建议在aboutToAppear里做一次去重用Set存储已有 id遇到重复的就跳过。5.3 ListItem 高度和间距的布局坑List 组件默认的行为是ListItem铺满整个 List 宽度但高度会根据内容自适应。如果在ListItem里直接设置height(100%)它参考的是父容器 List 的高度而不是内容高度列表会出现大量空白。正确做法是让卡片容器内部自适应或者给ListItem明确的固定高度。间距我建议用ListItem的margin而不是在卡片内部加padding。原因很简单列表项的 margin 控制的是卡片与卡片之间的距离视觉上更自然卡片内部的 padding 控制的是内容与卡片边缘的距离两者混用容易计算出奇怪的间距表现。5.4 环境数据展示位小数点处理传感器数据往往是小数直接显示45.666666很丑。处理方式是在展示格式化时保留一位小数用toFixed(1)。但注意toFixed返回的是字符串如果你将来要把这个值参与计算记得先parseFloat转回数字。这个细节不影响列表页功能但养成习惯可以避免后续做图表时的类型问题。6. 从列表页到整个地块管理系统的路线图数据模型和列表页完成后地块管理系统的地基已经算是打好了。后续的详情页可以直接复用这篇定义的PlotModel通过地块 id 拉取更完整的数据环境数据的历史曲线图也需要这块地里挂接的传感器数据作为数据源。所以写代码的时候要把模型文件、模拟数据文件跟页面文件分开不要让页面文件变成一个所有功能都往里塞的「大杂烩」后期维护的体验会好很多。我在实际开发中还有一个习惯每完成一个模块就把页面里的关键交互点、数据流走向在项目 README 里写清楚。这个习惯在项目规模小的时候看不出价值等模块多起来、需要回头查看某个列表的分页逻辑或筛选逻辑时能省下大量回忆时间。另外一个值得做的小事是把列表页的空数据状态和加载失败状态先占位。虽然现在模拟数据永远有内容但接入接口后一定会遇到后端返回空列表或者网络波动的情况。提前留好空状态提示比如暂无地块数据和重试按钮界面不会因为数据异常而出现空白。最后聊几句关于「技术选型的认识」。HarmonyOS 的开发范式跟传统 Web 前端或者 Android 原生都不太一样很多人初学时会拿习惯性的思路去套结果处处碰壁。其实调整一下心态ArkUI 的组件树 装饰器的状态管理模式跟 Vue 的响应式思想有相似之处跟 Flutter 的组件构建方式也有相通的地方。遇到问题的时候先想想手里的工具State、Prop、ForEach能做什么比硬背组件 API 更有效。到了这一步地块列表已经能在模拟器上流畅跑起来了。下一篇我会写地块详情页场地可视化布局、环境数据的动态刷新、设备关联信息展示。到时候你会发现这篇打好的模型基础会帮你节省大量重复工作。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询