ArkUI + HarmonyOS 响应式工单台:主从分栏与选中态(连载01/06)【共创稿事节】
一份工单列表,在手机上通常是一列内容;换到平板或折叠屏展开态,如果仍然只把手机页面居中放大,空白会变多,来回查看详情的成本却没有下降。真正需要解决的问题不是“屏幕宽了以后把按钮放大”,而是同一份业务数据如何自然地从单列阅读过渡为主从分栏,同时让当前选中项保持一致。
本篇围绕一个小型工单台建立最小可维护的布局骨架:左侧展示待处理工单,右侧展示当前详情;窄窗口中两块内容依次上下排列,中、大窗口中并列。这个基础会在后续继续用于筛选、窗口变化、状态恢复和性能优化。先用稳定的 ArkUI 栅格组件完成结构,不依赖尚未公开的特殊系统窗口接口。

核心决策:以窗口宽度决定排列,而不是以设备型号分支
习惯用设备型号区分布局的代码很容易变成这样:某手机走单列,某平板走双列,某折叠屏走另一套页面。这个策略的问题在于,同一台设备也可能有分屏、自由窗口、折叠/展开等多种窗口形态。设备名称不等于应用当前可用宽度。因此,布局应尽量围绕容器和断点建立规则,业务处理逻辑则不因窗口变化而改写。
ArkUI 的 GridRow 和 GridCol 支持在不同断点下设置栅格列数及子组件跨度。我们采用三组规则:sm 时总列数为 4,两块区域各占 4 列,天然换行;md 时总列数为 8,列表占 3 列、详情占 5 列;lg 时总列数为 12,列表占 4 列、详情占 8 列。这里的数字不是物理像素,更不是对某个机型的硬编码,而是相对栅格占比。
| 断点 | 总列数 | 工单列表 | 详情区域 | 呈现方式 |
|---|---|---|---|---|
| sm | 4 | 4 | 4 | 上下排列 |
| md | 8 | 3 | 5 | 左右分栏 |
| lg | 12 | 4 | 8 | 左右分栏 |
注意表格描述的是本文选定的布局规则,具体断点生效由容器所处环境决定;页面不能自行断言“手机一定是 sm、平板一定是 lg”。窗口宽度改变时,GridRow 会按可用条件重新计算布局。若把列表固定为 320px、详情固定为 800px,不仅破坏比例,还可能与系统字体缩放及窗口缩放冲突。

一份数据、一个选中状态:让分栏不产生两套业务逻辑
先明确数据结构。工单有稳定的 id、标题 title、状态 status 和说明 description。列表和详情使用同一数组,点击列表只更新一个 selectedId。不要分别为手机列表和平板详情维护两份“当前工单对象”,否则当筛选或异步刷新到来时,两份状态容易产生偏差。
下面给出页面 entry/src/main/ets/pages/Index.ets 的核心实现。使用 Stage 模型 ArkTS 工程中的普通 ArkUI 页面即可,示例只依赖系统内置的声明式 UI 组件,无额外三方依赖,也不需要新增网络或设备权限。
interface WorkOrder {
id: number;
title: string;
status: string;
description: string;
}
@Entry
@Component
struct Index {
@State private selectedId: number = 1;
@State private orders: WorkOrder[] = [
{ id: 1, title: '排查登录超时', status: '待处理',
description: '核对登录请求链路与超时提示。' },
{ id: 2, title: '修复列表筛选', status: '处理中',
description: '核对筛选条件及数据刷新范围。' },
{ id: 3, title: '梳理异常日志', status: '待处理',
description: '为异常事件补充可检索的分类字段。' }
];
private currentOrder(): WorkOrder | undefined {
return this.orders.find((item: WorkOrder) => item.id === this.selectedId);
}
@Builder
orderList() {
Column({ space: 10 }) {
Text('工单列表').fontSize(22).fontWeight(FontWeight.Bold)
List({ space: 8 }) {
ForEach(this.orders, (item: WorkOrder) => {
ListItem() {
Column({ space: 8 }) {
Text(item.title).fontSize(16).fontWeight(FontWeight.Medium)
Text(item.status).fontSize(13).fontColor('#64748B')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor(item.id === this.selectedId ? '#DBEAFE' : '#F1F5F9')
.onClick(() => { this.selectedId = item.id; })
}
}, (item: WorkOrder) => item.id.toString())
}
.height(320)
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor(Color.White)
}
@Builder
orderDetail() {
Column({ space: 16 }) {
Text('当前工单').fontSize(22).fontWeight(FontWeight.Bold)
if (this.currentOrder()) {
Text(this.currentOrder()!.title).fontSize(20)
Text(this.currentOrder()!.status).fontColor('#2563EB')
Text(this.currentOrder()!.description).fontSize(16)
} else {
Text('当前工单已不存在,请重新选择。').fontColor('#B45309')
}
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.height(416)
.padding(20)
.borderRadius(16)
.backgroundColor(Color.White)
}
build() {
Scroll() {
Column({ space: 18 }) {
Text('团队工单台').fontSize(28).fontWeight(FontWeight.Bold)
Text('选择左侧工单,查看对应详情')
.fontSize(14).fontColor('#64748B')
GridRow({
columns: { sm: 4, md: 8, lg: 12 },
gutter: { x: 12, y: 12 }
}) {
GridCol({ span: { sm: 4, md: 3, lg: 4 } }) {
this.orderList()
}
GridCol({ span: { sm: 4, md: 5, lg: 8 } }) {
this.orderDetail()
}
}
.width('100%')
}
.padding(16)
.width('100%')
.alignItems(HorizontalAlign.Start)
}
.backgroundColor('#F8FAFC')
.width('100%')
.height('100%')
}
}
这里特意保留了几个看起来“简单却容易被省略”的细节。列表的 ForEach 使用稳定 id 作为 key,而不是使用数组位置;未来加入排序、插入和删除操作时,位置可能变化,业务标识却不能跟着乱跳。selectedId 是页面级状态,点击后由 ArkUI 推动依赖它的文本和背景更新。详情不复制数据,而是调用 currentOrder() 从数组中查找当前对象。
更重要的是,列表与详情这两个区域在所有断点下都存在。布局变化仅改变空间关系,选中状态不会因为“切成平板布局”就重新初始化。这也是本系列后续讨论状态恢复时的基础。
实际交互从哪里观察,怎样排查错位
在窄窗口中,先看到工单列表,往下滚动看到详情;在较宽窗口中,列表与详情并排出现。改变 selectedId 后,高亮项与右侧标题应同时对应新的工单。下面的界面图是按代码关系制作的结构图,重点在布局与选中状态的映射,不用于替代设备运行记录。

建议在 DevEco Studio 中依次检查以下情况,别一上来就拿“横屏看着没问题”当验收结论:第一,改变预览窗口宽度,确认 sm/md/lg 栅格跨度符合设计;第二,切换选中项,确认两栏读到同一个 id;第三,在窄窗口向下滚动,确认详情不是被固定高度或非滚动容器裁掉;第四,替换为更长的中文标题,观察是否发生溢出;第五,删除选中工单后,确认详情能展示兜底内容。
这里有一个容易忽视的现象:假如列表选中了 id=2,后来筛选结果不再包含该记录,单纯保留 selectedId=2 就会让详情出现空态。空态并不是渲染框架的问题,而是业务状态已经不满足当前数据约束。正式项目中,应在数据集变化后校验选中项,或者设计一个明确的“保留选中但提示不可见”的产品规则。本篇先用 currentOrder() 的兜底分支确保不会直接访问不存在的对象。
另一个常见误区是把 GridCol 的 span 全部设置为与父级 columns 一样的值。这样虽然也能显示,却等价于始终占满一行,失去了真正的响应式分栏作用。华为的跨设备 Code Linter 规则也专门提示这一问题,因此不要把“用了 GridRow”直接视为“已经实现了自适应”。
工程边界与下一阶段的演进
这套结构适用于工单、邮件、知识库、文件浏览器等具有“列表—详情”关系的页面。它的优势是业务状态不与终端类型绑定、断点规则直观、列表和详情可以独立维护;局限是当前数据仍为内存示例、详情区使用了固定展示高度、窄屏仍采用上下阅读方式。面向真正的手机操作,下一阶段通常还需要明确“点开详情是否进入独立路由”,并处理窗口缩放、断点切换时选择状态的保存与恢复。
这一阶段的关键成果不是增加了多少 UI 控件,而是确立一条明确原则:断点只决定怎么排,选中状态决定看哪条数据。下一篇将把断点切换与选中态恢复拆开讨论,为可折叠窗口和多页面导航补上必要的衔接。
还可以给这份骨架补上一条很实用的评审标准:窗口变化之后,页面可以改变列数和信息密度,但不应悄悄改变业务含义。例如列表里一个蓝色高亮原本表示“当前工单”,切换到宽屏后也应继续表示同一条记录,不能突然变成“最近点击过的工单”。在团队协作开发中,最好将 selectedId 的所有写入位置限定在页面状态更新逻辑中,而不是散落在多个 UI 组件内部。这样将来接入路由、搜索和数据库时,仍能追踪每一次选择变化的来源。
对于滚动策略也需要预先定边界。当前示例将两区放在一个外层 Scroll 中,适合验证分栏结构;如果之后要承载数百条工单,列表区通常会使用独立可滚动列表,详情则有自己的阅读容器。否则滚动一个详情段落可能连左侧导航一起离开视野。这个选择没有唯一正确答案,核心是让用户知道滚动操作在改变哪片内容。
参考资料:
更多推荐

所有评论(0)