小雨同学 2026-05-29 09:30:08 发布前言
我在处理材料编辑页的时候,会注意到页面表单会不会被拉得太长。外屏状态下,一列表单从上到下排,标题、分类、提醒时间、处理备注、保存按钮都在同一条阅读路径里,用户填写完一个字段以后继续往下走,整个页面没有太多干扰。到了 Pura X Max 展开态以后,同样的表单如果继续铺满整个窗口,输入框会被拉成很长一条,短文本字段尤其空,页面看起来有空间,实际填写时反而不够集中。
这个问题在表单页里比列表页更明显。列表页变宽以后,可以考虑增加列数、增加字段或者做列表详情联动;表单页的目标不一样,它更在意填写路径和字段边界。输入框变宽以后,用户的视线会被拉得很散,短文本字段也会显得很空。保存按钮如果继续跟着表单铺满,页面底部也会变得有点松,用户很难判断真正的编辑区域从哪里开始、在哪里结束。
这类问题通常会出现在下面几种页面里:
- 材料编辑页
- 备注编辑页
- 提醒规则设置页
- 用户资料编辑页
- 订单或客户信息编辑页
- 轻量 CMS 内容维护页
我的处理方式是控制主表单宽度,多出来的空间交给辅助说明,比如来源信息、识别摘要、填写规则和当前状态。
这个页面我会拆成两种状态。外屏或宽度不足时,保持单列表单;展开态空间够用时,左侧显示受控宽度的主表单,右侧显示辅助说明卡片。这里最容易踩的坑,是只写一个宽度阈值就强行分栏。真正落到页面上,还要把左右 padding、中间间距、表单宽度和说明卡片宽度一起算进去。

一、表单被拉长以后哪里不对
1.1 外屏单列没有问题
编辑页在外屏里用单列结构很常见。标题在上方,下面依次是分类、提醒时间、备注和保存按钮。用户拿着设备时,从上往下填,字段之间的关系也比较清楚。这个场景里,输入框占满卡片宽度没有什么问题,因为窗口本来就窄,输入区域不会被拉得太离谱。
最开始的写法也很简单。
Column() { TextInput() TextInput() TextArea() Button()}.width('100%')这个结构在外屏没有问题。所有字段都跟随父容器宽度,开发时也不需要单独考虑左右区域。很多表单页最早都是这么写出来的,尤其是材料编辑、备注编辑、简单设置这类页面。
但是问题出现在展开态。窗口突然变宽以后,width('100%') 会把所有输入框都撑开。标题字段可能只有十几个字,输入框却占了一整行;分类字段可能只填两个字,输入区域也被拉得很长。页面看起来没有溢出,但填写节奏已经变散了。
1.2 展开态不能把空间都给输入框
我把编辑页放到展开态里看时,最明显的感觉是输入区域太宽。用户真正要填写的内容并不复杂,标题、分类、提醒时间都是短字段,备注也只是一段说明。把这些字段拉到接近整屏宽度,反而让表单看起来像一张空白很大的卡片。
表单页和卡片列表不一样。卡片列表可以通过多列展示更多记录,表单页更看重字段之间的连续性。用户填写时会反复确认字段名称和输入内容,如果输入框被拉得很长,字段标签在左边,输入内容在很宽的区域里展开,视线会被拖开。
我更倾向于把主表单控制在一个稳定宽度里。这个宽度不需要特别窄,但也不能跟着屏幕无限拉宽。多出来的横向空间可以用来放辅助说明,比如识别摘要、来源信息、当前状态、填写建议。这样展开态空间没有浪费,主表单也能保持比较稳定的填写路径。

二、分栏前先算可用宽度
2.1 不能只写一个展开态阈值
这个页面最容易写成一个简单判断:窗口超过某个宽度,就进入左右分栏。比如写一个 wideThreshold = 860,然后把表单放左边,说明卡片放右边。这个思路看起来直接,但实际跑起来容易在中间尺寸出问题。
左右分栏真正占用的宽度,不只是表单和说明卡片本身。页面左右 padding、中间间距、卡片阴影、滚动容器的可用宽度都会参与进来。如果只看窗口宽度,某些尺寸下页面会进入分栏,但两个卡片加起来已经接近或超过可用区域,最后就会出现横向挤压或溢出。
我这次把宽度拆开算。主表单给 540vp,右侧辅助说明给 260vp,中间间距给 14vp。页面左右 padding 在展开态下是 24vp。也就是说,窗口宽度必须先扣掉外层 padding,再去判断左右两块区域能不能同时放下。
private readonly wideThreshold: number = 860;private readonly formPanelWidth: number = 540;private readonly helperPanelWidth: number = 260;private readonly twoColumnGap: number = 14;这些值都不是固定模板。表单字段比较短,540vp 足够;如果页面里有长 URL、配置项、代码路径,表单可以加宽到 600vp 左右。右侧说明卡片如果只是展示状态和摘要,260vp 已经能承载;如果要放更复杂的说明,就要重新计算是否还能稳定分栏。
2.2 可用宽度要把 padding 算进去
真正决定是否进入左右分栏的函数是 canUseTwoColumn()。
private canUseTwoColumn(): boolean { const availableWidth = this.getEffectiveWidth() - this.getPagePadding() * 2; const requiredWidth = this.formPanelWidth + this.helperPanelWidth + this.twoColumnGap; return availableWidth >= requiredWidth;}这个函数里有两个量。availableWidth 是页面扣掉左右 padding 后能用的宽度,requiredWidth 是表单、说明卡片和间距加起来的宽度。只有前者大于等于后者,页面才进入左右分栏。
这比单纯判断 getEffectiveWidth() >= wideThreshold 更贴近真实布局。因为页面不是直接从屏幕边缘开始排内容,中间还有外层 padding。很多横向溢出的问题,恰恰出在开发时忘了把这些边距算进去。
我会把这个判断放在页面层,而不是让 FormPanel() 或 HelperPanel() 自己决定宽度。表单组件只负责显示字段,说明组件只负责显示辅助内容,是否左右排由外层统一决定。这样以后要改断点、改左右宽度,修改点会更集中。

三、控制表单宽度,说明放右侧
3.1 主表单只负责填写路径
表单进入左右分栏后,左侧仍然是主要区域。用户真正要完成的动作,是确认标题、分类、提醒时间和备注,然后保存编辑结果。这个区域不需要被拉到整屏宽,只要保持稳定、可读、可填写就够了。
示例里,左侧表单外层固定为 formPanelWidth。
Column() { this.FormPanel()}.width(this.formPanelWidth)FormPanel() 内部的输入框仍然使用 width('100%'),由父容器控制实际宽度。这样写比在每个输入框上写固定宽度更容易维护。以后如果要调整表单整体宽度,只改外层宽度就可以,不用挨个改输入框。
表单内部包括标题、分类、提醒时间、备注和保存按钮。每个字段仍然按上下顺序排列,不因为进入展开态就横向拆字段。我的经验是,简单编辑页不一定要把字段拆成多列。字段少的时候,单列表单更容易保持填写顺序,右侧空间交给辅助说明会更合适。
3.2 辅助说明只在空间够时出现
右侧说明卡片承接的是辅助信息,不参与主要填写路径。它可以展示来源、状态、建议动作、识别可信度,也可以放一段识别摘要。用户需要的时候可以看,不需要的时候也不会影响左侧表单的填写。
示例里的右侧卡片宽度是 helperPanelWidth。
Column() { this.HelperPanel()}.width(this.helperPanelWidth)右侧说明不要做得太复杂。它适合展示当前表单的上下文,比如这条材料来自哪里、识别出来了什么、建议保存成什么任务。如果把复杂编辑、更多表单字段或者长文本都塞到右侧,右侧就会变成另一个表单,左侧主表单和右侧说明之间的关系会变乱。

四、实际运行效果
这里我提供了外屏和展开态两个演示按钮,方便在同一台模拟器里观察表单布局变化。实际项目里可以删掉这些按钮,页面直接使用真实窗口宽度判断是否分栏。
外屏状态下,页面保持单列表单。所有字段从上到下排列,保存按钮仍然在表单内部。宽度不足时不强行分栏,可以避免表单和说明卡片互相挤压。

展开态状态下,页面进入左右结构。左侧表单宽度为 540vp,右侧说明卡片宽度为 260vp,中间间距为 14vp。外层 padding 已经被纳入宽度判断,页面不会因为固定宽度叠加而横向溢出。

五、实际项目中怎么用
5.1 演示宽度要删掉
示例里的 previewWidth 只是为了在同一个模拟器里观察外屏和展开态。真实项目里不需要这些演示按钮,页面应该直接使用真实窗口宽度。
private getEffectiveWidth(): number { if (this.previewWidth > 0) { return this.previewWidth; } return this.pageWidth;}实际项目时,可以直接返回 pageWidth。
private getEffectiveWidth(): number { return this.pageWidth;}页面宽度可以继续通过 onAreaChange 写入。这里记录的是页面根容器宽度,而不是设备型号。对 Pura X Max 来说,同一台设备可能处在外屏、展开态、分屏和自由窗口里,能不能分栏要看当前窗口实际能给多少空间。
5.2 表单宽度不要全局写死
示例里用了 formPanelWidth = 540,这是为了演示材料编辑页。真实项目里,表单宽度最好根据页面类型整理成配置。不同表单字段长度不同,适合的最大宽度也不一样。
比如:
- 资料编辑、提醒编辑这类短字段表单,可以控制在 520vp 到 560vp。
- 配置页、地址页、URL 输入页,可以适当放宽到 600vp 左右。
- 多字段复杂表单,如果字段之间有明显分组,可以考虑分段,而不是单纯加宽。
- 长文本编辑页不要套用短表单宽度,要单独设计阅读和编辑区域。
这个边界要提前想清楚。表单最大宽度不是一个全局万能值,它跟字段类型、输入内容长度、是否有右侧辅助说明都有关系。
5.3 右侧说明要服务表单
右侧说明卡片出现以后,不要随手把所有补充信息都塞进去。它应该帮助用户填写左侧表单,比如解释来源、展示识别摘要、提示建议动作。如果右侧卡片里放太多不相关内容,用户填写表单时反而会被干扰。
我会把右侧说明控制在几类内容里:
- 来源和状态
- 识别摘要
- 填写规则
- 建议动作
- 当前记录的轻量提醒
完整历史、附件、长说明、复杂校验结果,应该进入详情页或单独模块。右侧卡片只是辅助,不应该把主表单变成左右两边都要填写的复杂页面。
总结
Pura X Max 展开态里,表单页不能急着铺满整屏。外屏下单列表单更适合连续填写,字段从上到下排,用户不需要在左右区域之间来回找内容;展开态空间变宽以后,主表单反而要收住宽度,右侧再放识别摘要、来源状态和填写规则。这样处理以后,表单仍然是表单,右侧区域只是辅助,不会把编辑页变成一个横向拉长的输入区。
我后面处理这类编辑页时,需要注意:
- 主表单要有最大宽度,短字段不能跟着展开态无限拉长。
- 辅助说明只在空间足够时出现,不能和表单互相挤压。
- 分栏前要扣掉外层 padding、中间间距和两张卡片的固定宽度。
- 宽度不足时继续保持单列,不为了展开态强行左右分栏。
- 右侧说明只放来源、状态、识别摘要、填写规则这类辅助内容,不承接复杂编辑。
如果放到实际项目里,我会把表单宽度、说明卡片宽度和中间间距抽成配置,再按页面类型单独调整。短字段表单可以收得更窄,配置类表单可以适当放宽,长文本编辑页则要单独处理。展开态多出来的横向空间不一定都要给输入框,能帮助用户填写的内容才适合放到右侧。
附:完整代码
interface HelperItem { id: number; label: string; value: string;}@Entry@Componentstruct Index { // 页面真实宽度,由 onAreaChange 写入 @State private pageWidth: number = 0; // 演示宽度,只用于在同一个模拟器里观察外屏和展开态 @State private previewWidth: number = 0; // 表单字段模拟真实编辑状态 @State private materialTitle: string = '社区物业缴费提醒'; @State private category: string = '通知'; @State private remindTime: string = '2026-05-28 09:00'; @State private noteText: string = '缴费前一天提醒,并确认是否已经完成支付。'; // 模拟保存次数,用来观察操作状态是否正常保留 @State private saveCount: number = 0; private readonly wideThreshold: number = 860; private readonly formPanelWidth: number = 540; private readonly helperPanelWidth: number = 280; private readonly twoColumnGap: number = 16; private readonly helperItems: HelperItem[] = [ { id: 1, label: '来源', value: '拍照整理' }, { id: 2, label: '状态', value: '待处理' }, { id: 3, label: '建议动作', value: '保存为待办提醒' }, { id: 4, label: '识别可信度', value: '较高' } ]; // Demo 中优先使用演示宽度,真实项目里可以直接返回 pageWidth private getEffectiveWidth(): number { if (this.previewWidth > 0) { return this.previewWidth; } return this.pageWidth; } private getPagePadding(): number { if (this.getEffectiveWidth() >= this.wideThreshold) { return 24; } return 16; } // 分栏前先扣掉外层 padding,再判断表单、说明卡片和间距是否真的放得下 private canUseTwoColumn(): boolean { const width = this.getEffectiveWidth(); const availableWidth = width - this.getPagePadding() * 2; const requiredWidth = this.formPanelWidth + this.helperPanelWidth + this.twoColumnGap; return width >= this.wideThreshold && availableWidth >= requiredWidth; } private isExpanded(): boolean { return this.canUseTwoColumn(); } private getContentWidth(): Length { if (this.previewWidth > 0) { return this.previewWidth; } return '100%'; } private getTitleSize(): number { return this.isExpanded() ? 28 : 23; } private getModeText(): string { return this.isExpanded() ? 'expanded · 表单 + 说明分栏' : 'compact · 单列表单'; } private getModeDesc(): string { if (this.isExpanded()) { return '可用宽度足够时,主表单保持受控宽度,右侧显示辅助说明。'; } return '可用宽度不足时,表单保持单列,避免左右区域互相挤压。'; } private setPreview(width: number) { this.previewWidth = width; } private save() { this.saveCount += 1; } @Builder private PreviewButton(text: string, width: number) { Text(text) .fontSize(12) .fontColor(this.previewWidth === width ? '#FFFFFF' : '#2F8F83') .textAlign(TextAlign.Center) .padding({ left: 10, right: 10, top: 7, bottom: 7 }) .backgroundColor(this.previewWidth === width ? '#2F8F83' : '#E6F4F1') .borderRadius(999) .onClick(() => { this.setPreview(width); }) } @Builder private StatusPill(text: string) { Text(text) .fontSize(12) .fontColor('#B25E00') .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .backgroundColor('#FFF4E5') .borderRadius(999) } @Builder private DividerLine() { Divider() .strokeWidth(0.5) .color('#E5E7EB') .margin({ left: 92 }) } @Builder private HeaderPanel() { Column({ space: 10 }) { Row({ space: 10 }) { Column({ space: 4 }) { Text('编辑表单在展开态限制最大宽度') .fontSize(this.getTitleSize()) .fontWeight(FontWeight.Bold) .fontColor('#111827') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.getModeText()) .fontSize(14) .fontColor('#2F8F83') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) Text('窗口 ' + Math.round(this.pageWidth).toString() + 'vp') .fontSize(12) .fontColor('#374151') .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor('#FFFFFF') .borderRadius(999) } .width('100%') Text('演示宽度:' + Math.round(this.getEffectiveWidth()).toString() + 'vp。' + this.getModeDesc()) .fontSize(14) .fontColor('#6B7280') .lineHeight(21) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 8 }) { this.PreviewButton('自动', 0) this.PreviewButton('外屏', 430) this.PreviewButton('展开态', 1040) } .width('100%') } .width('100%') } @Builder private FormGroup() { Column() { Row({ space: 12 }) { Text('材料标题') .fontSize(15) .fontColor('#374151') .width(80) .flexShrink(0) TextInput({ text: this.materialTitle, placeholder: '请输入材料标题' }) .height(48) .layoutWeight(1) .fontSize(15) .fontColor('#111827') .placeholderColor('#9CA3AF') .backgroundColor('#00000000') .padding({ left: 0, right: 0 }) .onChange((value: string) => { this.materialTitle = value; }) } .width('100%') .height(56) .alignItems(VerticalAlign.Center) this.DividerLine() Row({ space: 12 }) { Text('分类') .fontSize(15) .fontColor('#374151') .width(80) .flexShrink(0) TextInput({ text: this.category, placeholder: '请输入分类' }) .height(48) .layoutWeight(1) .fontSize(15) .fontColor('#111827') .placeholderColor('#9CA3AF') .backgroundColor('#00000000') .padding({ left: 0, right: 0 }) .onChange((value: string) => { this.category = value; }) } .width('100%') .height(56) .alignItems(VerticalAlign.Center) this.DividerLine() Row({ space: 12 }) { Text('提醒时间') .fontSize(15) .fontColor('#374151') .width(80) .flexShrink(0) TextInput({ text: this.remindTime, placeholder: '请输入提醒时间' }) .height(48) .layoutWeight(1) .fontSize(15) .fontColor('#111827') .placeholderColor('#9CA3AF') .backgroundColor('#00000000') .padding({ left: 0, right: 0 }) .onChange((value: string) => { this.remindTime = value; }) } .width('100%') .height(56) .alignItems(VerticalAlign.Center) } .width('100%') .padding({ left: 14, right: 14 }) .backgroundColor('#F7F8FA') .borderRadius(18) } @Builder private NoteGroup() { Column({ space: 10 }) { Text('处理备注') .fontSize(15) .fontColor('#374151') TextArea({ text: this.noteText, placeholder: '请输入处理备注' }) .height(this.isExpanded() ? 124 : 104) .fontSize(15) .fontColor('#111827') .placeholderColor('#9CA3AF') .backgroundColor('#00000000') .padding({ left: 0, right: 0, top: 0, bottom: 0 }) .onChange((value: string) => { this.noteText = value; }) } .width('100%') .padding(14) .backgroundColor('#F7F8FA') .borderRadius(18) } @Builder private FormPanel() { Column({ space: 18 }) { Row() { Column({ space: 4 }) { Text('材料编辑') .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor('#111827') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text('按识别结果补全提醒信息') .fontSize(13) .fontColor('#6B7280') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) this.StatusPill('待处理') } .width('100%') this.FormGroup() this.NoteGroup() if (!this.isExpanded()) { Text('单列状态下,字段从上到下填写。宽度不足时不强行分栏,避免表单和说明卡片互相挤压。') .fontSize(13) .fontColor('#6B7280') .lineHeight(20) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } Button('保存编辑结果') .height(46) .fontSize(15) .fontColor('#FFFFFF') .width('100%') .backgroundColor('#2F8F83') .borderRadius(23) .onClick(() => { this.save(); }) Text('已保存 ' + this.saveCount.toString() + ' 次') .fontSize(13) .fontColor('#6B7280') .width('100%') .textAlign(TextAlign.Center) } .width('100%') .padding(this.isExpanded() ? 20 : 16) .backgroundColor('#FFFFFF') .borderRadius(26) .shadow({ radius: 12, color: '#12000000', offsetX: 0, offsetY: 4 }) } @Builder private HelperRow(item: HelperItem) { Column({ space: 5 }) { Text(item.label) .fontSize(12) .fontColor('#9CA3AF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(item.value) .fontSize(14) .fontColor('#374151') .lineHeight(20) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('100%') .padding(12) .backgroundColor('#F7F8FA') .borderRadius(16) } @Builder private HelperPanel() { Column({ space: 14 }) { Row() { Column({ space: 4 }) { Text('辅助说明') .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor('#111827') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text('识别内容和填写建议') .fontSize(13) .fontColor('#6B7280') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) } .width('100%') Text('展开态下,右侧只放辅助信息。主表单保持稳定宽度,填写动作仍然集中在左侧。') .fontSize(14) .fontColor('#4B5563') .lineHeight(22) .maxLines(4) .textOverflow({ overflow: TextOverflow.Ellipsis }) Column({ space: 10 }) { ForEach(this.helperItems, (item: HelperItem) => { this.HelperRow(item) }, (item: HelperItem) => item.id.toString()) } .width('100%') Column({ space: 8 }) { Text('识别摘要') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor('#111827') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text('识别到物业费缴纳截止日期、金额明细和办理地点。建议保存为待办提醒,并在截止日前一天通知。') .fontSize(14) .fontColor('#6B7280') .lineHeight(22) .maxLines(5) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('100%') .padding(14) .backgroundColor('#F3F8F7') .borderRadius(18) } .width('100%') .padding(16) .backgroundColor('#FFFFFF') .borderRadius(26) .shadow({ radius: 12, color: '#10000000', offsetX: 0, offsetY: 4 }) } @Builder private MainContent() { Scroll() { if (this.isExpanded()) { Row({ space: this.twoColumnGap }) { Column() { this.FormPanel() } .width(this.formPanelWidth) .flexShrink(0) Column() { this.HelperPanel() } .width(this.helperPanelWidth) .flexShrink(0) } .width('100%') .alignItems(VerticalAlign.Top) .justifyContent(FlexAlign.Center) .padding({ bottom: 24 }) } else { Column() { this.FormPanel() } .width('100%') .padding({ bottom: 24 }) } } .layoutWeight(1) .width('100%') .edgeEffect(EdgeEffect.Spring) } build() { Column() { Column({ space: 16 }) { this.HeaderPanel() this.MainContent() } .width(this.getContentWidth()) .height('100%') .padding({ left: this.getPagePadding(), right: this.getPagePadding(), top: 18, bottom: 16 }) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) .backgroundColor('#F6F7F9') .onAreaChange((_: Area, newValue: Area) => { const width = Number(newValue.width); if (!Number.isNaN(width) && width > 0) { this.pageWidth = width; } }) }}暂无评论数据
发布
相关推荐
栀枝夏
5445
0
li159
234
0
wuwuwu
160
0
●VON
860
0
深夜的构建者
287
0
小雨同学
产品总监、独立开发者社群主理人、资深全栈工程师,HarmonyOS应用开发者高级认证,PMP认证,CSDN博客专家,鸿蒙极客,Trae Fellow,阿里云社区专家博主、51CTO 博客专家、OpenTiny 优秀布道师、科大讯飞荣誉讲师。
帖子
提问
粉丝
鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 14:大屏弹窗改成侧边面板
2026-05-28 09:26:00 发布鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 13:顶部导航在窄窗口下如何简化
2026-05-27 09:09:23 发布
京公网安备:11010502051901号