一张报修看板放在手机上,看着是六张卡片组成的普通列表;把应用窗口拖到宽屏,开发人员通常希望它变成两列或三列。但这类看似纯粹的排版调整有个隐蔽的代价:如果把容器断点当成重新取数的信号,同一批工单可能会短暂消失,统计数字还会跟着闪一下。报修流程里,这种闪动比卡片挤在一起更影响判断。我把问题限定在一个可以独立讨论的场景:TicketBoard 工程的 BoardPage.ets 展示批次 RB-521,固定有六张工单,状态为待处理两张、处理中三张、已完成一张。目标不是写复杂的设备识别,而是让窗口宽度只决定排版,让数据仍由同一个列表负责。下文给出的是设计示例和预期展示数据,不是已经在某款设备上跑通的测试报告。## 一、先划开数据和布局的边界### 1. 宽度变化不等于业务重载应用进入多窗口后,屏幕尺寸与应用可用宽度不再是同一回事。平板的屏幕很宽,不代表当前窗口就宽;桌面上甚至可能同时存在好几个不同宽度的窗口。把“当前设备是平板”作为卡片列数的依据,到了分屏、自由窗口以及拖动边缘的场景就会失效。华为的 GridRow 允许用 breakpoints 指定断点,并通过 BreakpointsReference.WindowSize 让断点参照当前应用窗口宽度。配合 GridCol 的响应式 span,可以只做布局层的变化。这里故意不监听设备型号,也不根据屏幕旋转重新请求工单。它要解决的是组件变窄后排版重组,不是折叠态触发的表单草稿保存问题。### 2. 把工单做成唯一数据源先放下六条本地演示数据,给每一条稳定的 ticketId。不在渲染函数里按断点复制列表,也不在 onBreakpointChange 里过滤数据。统计信息从单一数组计算出来,这样才能讨论“守恒”:无论视觉上分成几列,待处理、处理中和完成的数量都应该保持 2、3、1。下面这段代码解决数据模型与计数口径。countByStatus 是纯函数,输入多少次相同的列表就得到相同的结果,方便未来接入真实接口时保留这一边界。tsinterface RepairTicket { ticketId: string; subject: string; status: string;}interface StatusCount { waiting: number; doing: number; done: number;}const demoTickets: RepairTicket[] = [ { ticketId: 'T-101', subject: '投影无信号', status: '待处理' }, { ticketId: 'T-102', subject: '走廊照明', status: '处理中' }, { ticketId: 'T-103', subject: '门禁不识别', status: '处理中' }, { ticketId: 'T-104', subject: '会议室空调', status: '已完成' }, { ticketId: 'T-105', subject: '打印机卡纸', status: '待处理' }, { ticketId: 'T-106', subject: '教室网络', status: '处理中' }];function countByStatus(rows: RepairTicket[]): StatusCount { return { waiting: rows.filter((t: RepairTicket) => t.status === '待处理').length, doing: rows.filter((t: RepairTicket) => t.status === '处理中').length, done: rows.filter((t: RepairTicket) => t.status === '已完成').length };}这里没有用工单在数组中的下标作为主键。后台刷新顺序后,同一张卡片仍然应由 T-101 这样的业务 ID 标识。状态类型在正式工程里可以进一步收窄为枚举或受约束的联合类型;本例保留字符串,是为了让六条夹具数据更容易阅读。重要的是计数函数不修改原数组,也不产生与窗口大小有关的分支。## 二、让 GridRow 自己处理列数### 1. 断点关注窗口,不参与请求GridRow 可指定 xs、sm、md、lg 各档列数。此处用四列、四列、八列、十二列的栅格总量;卡片在 xs、sm 档占满四列,在 md 档占四列,在 lg 档占四列。视觉效果因此是窄窗口一行一张、中窗口一行两张、宽窗口一行三张。断点阈值采用官方文档给出的 320vp、600vp、840vp,只是演示起点;实际产品还要依据字体大小、卡片最小宽度、横竖屏与无障碍放大做调整。这段页面代码展示布局与状态提示的连接方式。onBreakpointChange 只写入当前断点用于观察,既不清空 tickets,也不调用网络请求。页面中只引用一个 tickets 数据源,UI 层用稳定的 ticketId 作为 ForEach 键。ts@Entry@Componentstruct BoardPage { @State tickets: RepairTicket[] = demoTickets; @State currentBp: string = 'sm'; private batchId: string = 'RB-521'; build() { Column({ space: 12 }) { Text('TicketBoard · 报修看板').fontSize(22).fontWeight(FontWeight.Bold) Text(`批次 ${this.batchId} · 断点 ${this.currentBp}`) GridRow({ columns: { xs: 4, sm: 4, md: 8, lg: 12 }, breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize } }) { ForEach(this.tickets, (ticket: RepairTicket) => { GridCol({ span: { xs: 4, sm: 4, md: 4, lg: 4 } }) { Column({ space: 6 }) { Text(ticket.ticketId).fontColor('#58667A') Text(ticket.subject).fontWeight(FontWeight.Medium) Text(ticket.status).fontColor('#157C68') } .width('100%').padding(12).backgroundColor('#F2F6F9') } }, (ticket: RepairTicket) => ticket.ticketId) } .onBreakpointChange((bp: string) => { this.currentBp = bp; }) }.padding(16).width('100%') }}````GridRow` 不是通用的状态容器,只负责布局。页面重绘时并不意味着后台工单对象要重建。如果实际项目采用远程数据,建议将加载结果和查询条件放在独立状态层,避免渲染重建触发副作用。`currentBp` 是诊断字段,可以显示在开发版页面上,正式发布时可以隐藏。不要把官方默认断点误认为全部机型的最佳设计参数,更不能把 `WindowSize` 理解成物理屏幕尺寸。![](https://i-blog.csdnimg.cn/direct/7093337806784bbfaea82042bba5b907.png)## 三、把“统计守恒”变成可核对的条件### 1. 同一批数据只计算一次口径静态演示数据可以直接算出六张工单、待处理两张、处理中三张、已完成一张。宽度从 `sm` 进入 `lg` 时改变的是每行放几张,不应改变这几个业务数字。调试时,不用盯着每个卡片的像素位置;先看列表总数、三个分组数以及批次 ID,有助于排除数据层的问题。下面是建议放在调试入口的一个检查函数。它把“统计是否和总数对得上”写为明确条件,并返回简短的日志文本。该字符串用于本轮演示图,**不是声称已经在 HiLog 捕获的真实日志**。tsfunction buildAuditLine(batchId: string, bp: string, rows: RepairTicket[]): string { const count: StatusCount = countByStatus(rows); const sum: number = count.waiting + count.doing + count.done; if (sum !== rows.length) { throw new Error(status count mismatch: ${sum}/${rows.length}); } return [DEMO] ${batchId} bp=${bp} cards=${rows.length} + waiting=${count.waiting} doing=${count.doing} done=${count.done};}const previewLine: string = buildAuditLine(‘RB-521’, ‘sm’, demoTickets);// [DEMO] RB-521 bp=sm cards=6 waiting=2 doing=3 done=1```如果以后增加“暂停派单”等第四种状态,这个函数会因为总数不匹配而报错。此处抛错是开发期的契约检查,不建议放在不能恢复的线上交互路径;正式工程应记录可追踪的告警,并根据产品流程明确是否展示未归类项。另一个常见误区是把日志里的 bp=sm 当成设备判断。它只说明这一时刻的布局断点,与设备品牌、屏幕总尺寸和应用是否折叠没有直接对应关系。### 2. 看一眼窄屏演示的取舍手机演示图把 RB-521 放在批次行,最重要的是六张工单保持原来的顺序,顶部总数与分组数字不变。窄窗口下只显示一列,是为了让工单标题和状态标签不互相挤压。宽窗口如果重排为三列,操作控件需要跟随卡片的实际宽度,而不是简单把手机界面等比拉大。## 四、真正上线前还有三件事要验其一是窗口反复缩放。不能只测试从全屏进入分屏一次;应连续拖动窗口宽度跨过相邻断点,观察重排是否抖动,以及输入框焦点、滚动位置、菜单弹层是否受到影响。onBreakpointChange 触发多次是正常的,业务层不能拿它当“刷新列表”的唯一入口。窗口改变时涉及的滚动锚点问题需要另外设计,并非这段样例已经解决。其二是数据更新与断点变化同时发生。真实报修系统可能在网络响应返回时恰好被用户收窄窗口。列表更新应以请求版本和查询条件做检查,视觉重排交给栅格;否则,较旧的一页结果可能覆盖刚刚更新的状态。本文不引入请求序号,因为那会把一个布局问题带向另一个异步任务设计问题,超出本篇的控制范围。其三是可访问性。字体放大、长工单名、不同语言环境下的状态文案都可能导致栅格高度不齐。不要只依据演示图判断真实产品的可用性,还需在目标 SDK、DevEco Studio、模拟器与设备上验证 GridRow 的最终表现。断点的最优值也应根据页面内容和实际可用宽度选取,而非照搬演示参数。回到最开始的目标:六条业务数据由一个数组维护,断点只改变 GridCol 的宽度分配。可以变化的是排版,不能被排版偷偷改掉的是工单事实。 这一点比“到底几列更好看”更值得写进代码审查标准。官方参考(检索于 2026-10-18):华为开发者文档《Responsive Grid Layout (GridRow/GridCol)》,https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-layout-development-grid-layout 。本文仅演示 ArkUI 栅格布局与本地夹具计数,不代表已完成应用编译、窗口缩放实测或性能验证。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐