前言我在调材料编辑页的时候会注意到页面表单会不会被拉得太长。外屏状态下一列表单从上到下排标题、分类、提醒时间、处理备注、保存按钮都在同一条阅读路径里用户填写完一个字段以后继续往下走整个页面没有太多干扰。到了 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 Component struct 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; } }) } }
鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 15:编辑表单在展开态限制最大宽度
发布时间:2026/6/23 2:36:58
前言我在调材料编辑页的时候会注意到页面表单会不会被拉得太长。外屏状态下一列表单从上到下排标题、分类、提醒时间、处理备注、保存按钮都在同一条阅读路径里用户填写完一个字段以后继续往下走整个页面没有太多干扰。到了 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 Component struct 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; } }) } }