鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 11:分屏窗口下布局自动降级

前言

我在调材料列表页的时候,最早关注的是展开态双栏。左边放列表,右边放详情,点一条记录以后右侧直接切换内容,这种结构在 Pura X Max 展开态里确实能减少页面跳转。尤其是材料整理、会议纪要、客户记录这类页面,用户经常需要在多条记录之间来回看,列表和详情同时出现会省掉不少切换动作。

但我把应用拖进分屏以后,页面的状态就变了。原来能放下左侧列表和右侧详情的宽度,被分屏切掉一部分以后,左侧列表开始变窄,右侧详情也跟着压缩。标题还能显示,按钮也还能点击,可每个区域都变得勉强,左边不像列表,右边也不像详情。

Pura X Max 外屏是 5.4 英寸,内屏是 7.7 英寸,展开态确实适合承载更多信息;同时 HarmonyOS 支持全屏、分屏、自由窗口这几种窗口形态,同一个页面随时可能从完整宽窗口变成窄窗口。也就是说,双栏不能当成页面的固定结构,它更像是一种"宽窗口下才启用的能力"。窗口一旦缩窄,页面要能退回单栏;如果窗口再窄一点,卡片内部还要继续收起辅助字段。

这次我把页面分成三种状态来处理:

  • 宽窗口:左侧列表,右侧详情
  • 窄窗口:当前记录在上,列表在下
  • 极窄窗口:只保留标题、状态和主操作

这三个状态让分屏以后页面还保留基本可用性,宽的时候展示更多,窄的时候先保证能读、能点、能继续处理当前记录。

一、双栏在分屏里会先挤压内容

1.1 展开态双栏没有问题

我一开始做展开态页面时,会很自然地想到列表详情结构。左侧列表负责切换,右侧详情负责展示当前记录,这个结构在宽窗口里很有价值。

材料整理页就是典型场景。用户可能需要连续看几条材料,点左侧记录,右侧详情马上变化,不用每次都进入新页面再返回列表。宽度足够时,这种结构能减少页面层级,也能让用户保留上下文。

在代码上,这种结构通常就是一个 Row

arkts 复制代码
Row({ space: 18 }) {
  this.ListPanel()
  this.DetailPanel(this.getSelectedItem())
}

宽窗口里这样写没问题。左侧给一个相对固定的列表宽度,右侧吃掉剩余空间,页面能同时呈现两种信息。这个结构的问题出现在窗口缩窄以后。

1.2 分屏以后两边都开始吃紧

我把页面切到分屏宽度后,第一眼看到的是左侧列表卡片变得很窄。标题本来可以显示两行,缩窄以后只剩一小段,摘要也被截断。右侧详情区域虽然还在,但正文换行变多,按钮和状态信息挤在一起,整个页面读起来很费劲。

这个时候页面并不是完全不可用,真正麻烦的是两个区域都只能勉强工作。左侧列表失去了快速扫读的能力,右侧详情也没有足够空间承载完整内容。继续保留双栏,反而让列表和详情互相抢宽度。

我在分屏里会先看这几个点:

  • 左侧列表的标题还能不能读完整
  • 右侧详情是否还能承载正文
  • 主按钮有没有被挤到难点的位置
  • 用户是否还需要同时看到列表和详情

当这些条件都开始变差时,我不会继续保留双栏。这个页面更适合退回单栏,让当前记录先占据上方区域,再把列表放到下方。这样做虽然少了一点"大屏感",但用户至少还能正常处理当前材料。

二、我把页面分成三档

2.1 宽窗口保留双栏

宽窗口下,我仍然保留双栏结构。Pura X Max 展开态有足够横向空间时,左侧列表和右侧详情并排出现,用户能一边切换记录,一边查看详情内容。

这时列表宽度可以固定在一个相对稳定的范围,比如 330vp。右侧详情区域使用剩余空间,负责展示标题、摘要、来源、负责人、正文和主操作。

arkts 复制代码
private readonly wideWidth: number = 820;

我把 820vp 作为宽窗口门槛。这个值不是固定标准,真实项目里可以根据列表宽度、详情内容和页面边距调整。材料列表标题比较长,就要多留一些空间;右侧详情字段比较少,门槛可以稍微降低。

2.2 窄窗口退回单栏

窗口缩窄到中间状态时,我不会继续保留双栏。页面会退回单栏,上方先显示当前处理记录,下方再显示列表。

这种结构牺牲了列表详情同时展示,但换来的是当前内容区域不会被左右挤压。用户在分屏里更可能是在处理一条具体记录,而不是反复浏览大量详情。把当前记录放在上方,可以让用户先完成眼前这件事。

中间状态用 narrowWidth 控制:

arkts 复制代码
private readonly narrowWidth: number = 520;

当窗口宽度达到 520vp,但还没有达到 820vp 时,页面进入 narrow。这个状态下,当前处理卡片会显示标题、状态、摘要和主按钮,列表继续放在下方。

2.3 极窄窗口只留核心内容

再往下缩,窗口进入极窄状态。这个时候继续展示摘要、来源、时间、标签,页面会显得很挤。极窄窗口更像一个临时处理入口,不适合承担完整信息展示。

我会把卡片内部再收一层,只保留标题、状态和主按钮。摘要、来源、时间这些信息暂时隐藏,等窗口恢复到更宽状态时再显示。

arkts 复制代码
private getLayoutMode(): string {
  const width = this.getEffectiveWidth();

  if (width >= this.wideWidth) {
    return 'wide';
  }

  if (width >= this.narrowWidth) {
    return 'narrow';
  }

  return 'tiny';
}

这里把页面分成 wide、narrow、tiny 三种状态。真实项目里可以把这些字符串换成枚举,或者抽到统一的布局工具里。这个示例先保持简单,重点是把降级规则集中到一个函数里,避免 UI 分支散落在各个组件里。

三、状态要留在页面层

3.1 选中记录不能跟丢布局

分屏降级不只是布局变化。用户正在处理哪条记录,也要保留下来。

我在这个示例里保留了 selectedId。无论页面处于 wide、narrow 还是 tiny,当前选中的记录都从同一个状态里读取。

arkts 复制代码
@State private selectedId: number = 1;

private getSelectedItem(): MaterialItem {
  const found = this.materials.find((item: MaterialItem) => item.id === this.selectedId);
  return found ? found : this.materials[0];
}

这样做的好处是,用户在宽窗口里选中某条记录后,切到窄窗口或者极窄窗口,当前处理内容仍然是同一条。布局变了,正在处理的上下文没有丢。

真实项目里这个点很容易被忽略。如果选中态放在某个具体布局组件里,比如只放在右侧详情面板里,那么面板一旦消失,状态可能也跟着重置。把状态放到页面层,会省掉很多切换时的补丁逻辑。

3.2 操作次数也不能重置

还有一个 actionCount,用来模拟用户点击主按钮后的操作次数。

arkts 复制代码
@State private actionCount: number = 0;

private handleAction() {
  this.actionCount += 1;
}

这个状态看起来很简单,但它可以用来验证布局切换时状态是否保留。宽窗口里点击操作按钮后,再切到窄窗口或极窄窗口,操作次数仍然存在。真实项目里,这类状态可能是保存进度、已读状态、待提交标记、正在处理的任务 ID。

分屏适配如果只处理布局,很容易遗漏这些上下文状态。我的习惯是先把页面状态放到外层,再让不同布局去消费这些状态。这样 wide、narrow、tiny 三种展示方式可以变化,业务状态仍然是一份。

四、实际运行效果

这里我提供了"宽屏""窄屏""极窄"三个演示按钮,主要是为了在同一个模拟器里快速观察布局降级。真实项目里不需要这些按钮,页面会直接跟随真实窗口宽度变化。

宽屏状态下,页面显示左列表、右详情。这个状态适合 Pura X Max 展开态,或者任何足够宽的窗口。左侧记录负责切换,右侧详情展示当前内容。

窄屏状态下,页面退回单栏。上方是当前处理记录,下方是材料列表。这个状态更接近分屏后的中等窗口,详情不再和列表并排,当前记录先被放到页面顶部。

极窄状态下,卡片内部继续收起辅助信息。标题、状态和主按钮保留,摘要、来源、时间等信息暂时隐藏。这个状态适合更窄的分屏比例、小尺寸自由窗口,或者只需要快速处理当前记录的场景。

五、实际放在项目中

5.1 演示宽度要删掉

这里我用了 previewWidth 和几个演示按钮,方便在一台模拟器里观察三种状态。真实项目里不需要这些东西。

arkts 复制代码
private getEffectiveWidth(): number {
  if (this.previewWidth > 0) {
    return this.previewWidth;
  }

  return this.pageWidth;
}

迁回真实项目时,可以直接返回 pageWidth

arkts 复制代码
private getEffectiveWidth(): number {
  return this.pageWidth;
}

宽度仍然可以通过 onAreaChange 获取。这个事件会在组件区域变化时触发,适合记录页面根容器当前宽度。注意这里记录的是组件区域变化,不是设备型号变化。

5.2 降级规则可以抽出去

如果项目里有多个页面都要处理分屏降级,我不建议每个页面都复制 getLayoutMode()。可以把 wide、narrow、tiny 这类断点规则抽到一个工具里。

比如:

arkts 复制代码
private getLayoutMode(): string {
  const width = this.getEffectiveWidth();

  if (width >= this.wideWidth) {
    return 'wide';
  }

  if (width >= this.narrowWidth) {
    return 'narrow';
  }

  return 'tiny';
}

真实项目里可以把它改成枚举,或者统一放到布局配置中。列表页、详情页、表单页可以共享同一套基础断点,但每个页面内部显示什么字段,仍然要根据业务决定。

5.3 tiny 状态不要承载太多内容

极窄窗口里,我只保留标题、状态和主操作。这个取舍很直接,因为窗口已经没有足够空间承载摘要、来源、负责人这些辅助字段。

如果 tiny 状态还要继续放完整摘要、多个标签、来源时间和次要操作,页面会变成一堆挤在一起的内容块。这里我宁愿让用户先完成主操作,更多信息等窗口变宽后再显示,或者进入详情页处理。

这条规则可以迁移到很多页面里。悬浮窗、极窄分屏、临时窗口,都不适合展示完整内容。它们更适合保留一件事:当前处理对象是什么,以及用户下一步能做什么。

总结

Pura X Max 展开态适合双栏,但分屏以后,双栏不一定还能成立。宽窗口下左列表右详情可以减少跳转;窗口缩窄后,单栏会给当前记录留下更多空间;再窄一些时,卡片内部也要继续收起辅助字段,只保留标题、状态和主操作。

我处理这类页面时,会把双栏当成宽窗口能力,而不是默认结构。页面宽的时候展示更多,窄的时候先保证当前记录能读、能点、能继续处理。布局在变,选中记录和操作状态要保留下来,这一点比单纯切换 RowColumn 更容易被忽略。

附:完整代码

arkts 复制代码
interface MaterialItem {
  id: number;
  title: string;
  status: string;
  source: string;
  time: string;
  tag: string;
  owner: string;
  summary: string;
  detail: string;
  action: string;
}

@Entry
@Component
struct Index {
  // 页面真实宽度,由 onAreaChange 写入
  @State private pageWidth: number = 0;

  // 演示宽度,只用于在同一个模拟器里切换 wide / narrow / tiny 三种状态
  @State private previewWidth: number = 0;

  // 当前选中记录放在页面层,避免布局切换后丢失上下文
  @State private selectedId: number = 1;

  // 模拟操作次数,用来观察布局降级后操作状态是否保留
  @State private actionCount: number = 0;

  // narrow 以下进入极窄状态,wide 以上才显示列表详情双栏
  private readonly narrowWidth: number = 520;
  private readonly wideWidth: number = 820;

  private readonly materials: MaterialItem[] = [
    {
      id: 1,
      title: '社区物业缴费提醒',
      status: '待处理',
      source: '拍照整理',
      time: '09:20',
      tag: '通知',
      owner: '物业服务中心',
      summary: '识别到缴费截止日期、金额明细和办理地点。',
      detail: '这条记录来自一张社区物业缴费通知。全屏展开态下适合左侧列表、右侧详情;分屏变窄后,页面退回单栏会给当前记录留下更多空间。',
      action: '添加提醒'
    },
    {
      id: 2,
      title: 'Pura X Max 适配会议纪要',
      status: '待确认',
      source: '语音转写',
      time: '10:45',
      tag: '会议',
      owner: '产品研发组',
      summary: '整理出分屏、横屏、悬停态和详情页适配任务。',
      detail: '会议记录类页面经常需要在列表中连续切换。宽窗口下可以使用左右双栏,窗口缩窄后回到单栏,当前记录会更容易阅读。',
      action: '确认任务'
    },
    {
      id: 3,
      title: '客户需求变更记录',
      status: '待处理',
      source: '文本整理',
      time: '13:10',
      tag: '项目',
      owner: '客户成功组',
      summary: '本次变更涉及首页布局、权限配置和消息提醒。',
      detail: '需求变更记录通常需要查看完整说明和处理动作。分屏后继续保留双栏,左侧列表和右侧详情都会被压缩。',
      action: '同步排期'
    },
    {
      id: 4,
      title: '活动报名确认单',
      status: '已保存',
      source: '相册导入',
      time: '15:25',
      tag: '表单',
      owner: '活动运营',
      summary: '提取到报名人、联系方式、活动时间和签到地址。',
      detail: '报名确认类材料字段较多,极窄窗口下只保留标题、状态和主操作,其他信息放到更宽窗口或详情中展示。',
      action: '加入日程'
    },
    {
      id: 5,
      title: '门诊复查预约提示',
      status: '已整理',
      source: '拍照整理',
      time: '16:40',
      tag: '提醒',
      owner: '个人记录',
      summary: '提取到复查时间、科室、楼层和注意事项。',
      detail: '提醒类记录在窄窗口中更适合快速处理,不需要把所有辅助字段都展示出来。',
      action: '保存提醒'
    }
  ];

  // Demo 中优先使用演示宽度,真实项目里可以直接返回 pageWidth
  private getEffectiveWidth(): number {
    if (this.previewWidth > 0) {
      return this.previewWidth;
    }

    return this.pageWidth;
  }

  // 把布局状态集中在一个函数里,避免 UI 分支散落到每个组件内部
  private getLayoutMode(): string {
    const width = this.getEffectiveWidth();

    if (width >= this.wideWidth) {
      return 'wide';
    }

    if (width >= this.narrowWidth) {
      return 'narrow';
    }

    return 'tiny';
  }

  private isWide(): boolean {
    return this.getLayoutMode() === 'wide';
  }

  private isNarrow(): boolean {
    return this.getLayoutMode() === 'narrow';
  }

  private isTiny(): boolean {
    return this.getLayoutMode() === 'tiny';
  }

  private getContentWidth(): Length {
    if (this.previewWidth > 0) {
      return this.previewWidth;
    }

    return '100%';
  }

  private getPagePadding(): number {
    if (this.isWide()) {
      return 24;
    }

    if (this.isNarrow()) {
      return 16;
    }

    return 12;
  }

  private getTitleSize(): number {
    if (this.isWide()) {
      return 28;
    }

    if (this.isNarrow()) {
      return 23;
    }

    return 20;
  }

  private getModeText(): string {
    if (this.isWide()) {
      return 'wide · 双栏布局';
    }

    if (this.isNarrow()) {
      return 'narrow · 单栏布局';
    }

    return 'tiny · 核心内容';
  }

  private getModeDesc(): string {
    if (this.isWide()) {
      return '宽窗口下显示左侧列表和右侧详情。';
    }

    if (this.isNarrow()) {
      return '窄窗口下当前记录在上,列表在下。';
    }

    return '极窄窗口下只保留标题、状态和主操作。';
  }

  private getSelectedItem(): MaterialItem {
    const found = this.materials.find((item: MaterialItem) => item.id === this.selectedId);
    return found ? found : this.materials[0];
  }

  private setPreview(width: number) {
    this.previewWidth = width;
  }

  private handleAction() {
    this.actionCount += 1;
  }

  private getStatusColor(status: string): string {
    if (status === '待处理') {
      return '#B25E00';
    }

    if (status === '待确认') {
      return '#7C3AED';
    }

    return '#276749';
  }

  private getStatusBgColor(status: string): string {
    if (status === '待处理') {
      return '#FFF4E5';
    }

    if (status === '待确认') {
      return '#F1EAFE';
    }

    return '#E7F5EE';
  }

  @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(status: string) {
    Text(status)
      .fontSize(12)
      .fontColor(this.getStatusColor(status))
      .padding({ left: 8, right: 8, top: 4, bottom: 4 })
      .backgroundColor(this.getStatusBgColor(status))
      .borderRadius(999)
  }

  @Builder
  private MetaPill(text: string) {
    Text(text)
      .fontSize(12)
      .fontColor('#4B5563')
      .padding({ left: 8, right: 8, top: 4, bottom: 4 })
      .backgroundColor('#F3F4F6')
      .borderRadius(999)
  }

  @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('宽屏', 960)
        this.PreviewButton('窄屏', 640)
        this.PreviewButton('极窄', 420)
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  private MaterialCard(item: MaterialItem) {
    Column({ space: this.isTiny() ? 10 : 12 }) {
      Row({ space: 8 }) {
        this.StatusPill(item.status)

        if (!this.isTiny()) {
          this.MetaPill(item.tag)
        }

        Blank()

        if (this.selectedId === item.id) {
          Text('当前')
            .fontSize(12)
            .fontColor('#2F8F83')
        }
      }
      .width('100%')

      Text(item.title)
        .fontSize(this.isTiny() ? 16 : 17)
        .fontWeight(FontWeight.Medium)
        .fontColor('#111827')
        .maxLines(this.isTiny() ? 1 : 2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      if (!this.isTiny()) {
        Text(item.summary)
          .fontSize(13)
          .fontColor('#6B7280')
          .lineHeight(19)
          .maxLines(this.isWide() ? 2 : 1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Row({ space: 8 }) {
          Text(item.source)
            .fontSize(12)
            .fontColor('#6B7280')

          Text('·')
            .fontSize(12)
            .fontColor('#9CA3AF')

          Text(item.time)
            .fontSize(12)
            .fontColor('#6B7280')
        }
        .width('100%')
      }

      if (this.isTiny()) {
        Button(item.action)
          .fontSize(13)
          .fontColor('#FFFFFF')
          .height(34)
          .width('100%')
          .backgroundColor('#2F8F83')
          .borderRadius(17)
          .onClick(() => {
            this.selectedId = item.id;
            this.handleAction();
          })
      }
    }
    .width('100%')
    .padding(this.isTiny() ? 12 : 15)
    .backgroundColor(this.selectedId === item.id ? '#EEF7F5' : '#FFFFFF')
    .borderRadius(this.isTiny() ? 16 : 18)
    .border({
      width: this.selectedId === item.id ? 1.5 : 1,
      color: this.selectedId === item.id ? '#2F8F83' : '#E5E7EB'
    })
    .shadow({
      radius: this.selectedId === item.id ? 10 : 7,
      color: '#10000000',
      offsetX: 0,
      offsetY: 4
    })
    .onClick(() => {
      this.selectedId = item.id;
    })
  }

  @Builder
  private ListPanel() {
    Scroll() {
      Column({ space: 12 }) {
        ForEach(this.materials, (item: MaterialItem) => {
          this.MaterialCard(item)
        }, (item: MaterialItem) => item.id.toString())
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .layoutWeight(1)
    .width('100%')
    .edgeEffect(EdgeEffect.Spring)
  }

  @Builder
  private DetailPanel(item: MaterialItem) {
    Column({ space: 16 }) {
      Row() {
        this.StatusPill(item.status)

        Blank()

        this.MetaPill(item.tag)
      }
      .width('100%')

      Text(item.title)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#111827')
        .lineHeight(31)

      Text(item.summary)
        .fontSize(15)
        .fontColor('#4B5563')
        .lineHeight(23)

      Row({ space: 10 }) {
        this.MetaBlock('来源', item.source)
        this.MetaBlock('时间', item.time)
        this.MetaBlock('负责人', item.owner)
      }
      .width('100%')

      Column({ space: 8 }) {
        Text('内容详情')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#111827')

        Text(item.detail)
          .fontSize(15)
          .fontColor('#4B5563')
          .lineHeight(24)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#F9FAFB')
      .borderRadius(18)

      Column({ space: 8 }) {
        Text('操作状态')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#111827')

        Text('当前操作已触发 ' + this.actionCount.toString() + ' 次。切换窗口宽度后,选中项和操作状态仍然保留。')
          .fontSize(14)
          .fontColor('#6B7280')
          .lineHeight(22)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#F3F8F7')
      .borderRadius(18)

      Blank()

      Button(item.action)
        .fontSize(15)
        .fontColor('#FFFFFF')
        .height(44)
        .width('100%')
        .backgroundColor('#2F8F83')
        .borderRadius(22)
        .onClick(() => {
          this.handleAction();
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(24)
    .shadow({
      radius: 12,
      color: '#12000000',
      offsetX: 0,
      offsetY: 4
    })
  }

  @Builder
  private MetaBlock(label: string, value: string) {
    Column({ space: 4 }) {
      Text(label)
        .fontSize(12)
        .fontColor('#9CA3AF')

      Text(value)
        .fontSize(14)
        .fontColor('#374151')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .layoutWeight(1)
    .padding(12)
    .backgroundColor('#F9FAFB')
    .borderRadius(14)
  }

  @Builder
  private CompactSelectedPanel(item: MaterialItem) {
    Column({ space: 10 }) {
      Row() {
        Text('当前处理')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#111827')

        Blank()

        this.StatusPill(item.status)
      }
      .width('100%')

      Text(item.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#111827')
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      if (this.isNarrow()) {
        Text(item.summary)
          .fontSize(14)
          .fontColor('#4B5563')
          .lineHeight(21)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }

      Button(item.action)
        .fontSize(14)
        .fontColor('#FFFFFF')
        .height(40)
        .width('100%')
        .backgroundColor('#2F8F83')
        .borderRadius(20)
        .onClick(() => {
          this.handleAction();
        })
    }
    .width('100%')
    .padding(this.isTiny() ? 12 : 16)
    .backgroundColor('#FFFFFF')
    .borderRadius(20)
    .shadow({
      radius: 10,
      color: '#10000000',
      offsetX: 0,
      offsetY: 4
    })
  }

  @Builder
  private MainContent() {
    if (this.isWide()) {
      Row({ space: 18 }) {
        Column() {
          this.ListPanel()
        }
        .width(330)
        .height('100%')

        Column() {
          this.DetailPanel(this.getSelectedItem())
        }
        .layoutWeight(1)
        .height('100%')
      }
      .width('100%')
      .height('100%')
    } else {
      Column({ space: 14 }) {
        this.CompactSelectedPanel(this.getSelectedItem())

        Column() {
          this.ListPanel()
        }
        .layoutWeight(1)
        .width('100%')
      }
      .width('100%')
      .height('100%')
    }
  }

  build() {
    Column() {
      Column({ space: 16 }) {
        this.HeaderPanel()

        Column() {
          this.MainContent()
        }
        .layoutWeight(1)
        .width('100%')
      }
      .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;
      }
    })
  }
}
相关推荐
郑寿昌12 小时前
华为韬定律与摩尔定律深度对比
华为
努力搬砖的鱼12 小时前
深信服aTrust集群从节点访问失败排查:交换机策略重定向引发的“回包丢失”之谜
运维·网络·华为
松☆12 小时前
用昇腾NPU给鸿蒙设备跑推理,全流程实录
华为·harmonyos
nashane13 小时前
HarmonyOS 6学习:异步操作中Toast提示框消失之谜与UIContext解决方案实战
学习·华为·harmonyos
久菜盒子工作室14 小时前
华为“韬(τ)定律”
华为
松☆16 小时前
Triton推理服务接昇腾NPU,GE后端怎么搭?
华为·性能优化·numpy·信号处理·harmonyos
lqj_本人17 小时前
鸿蒙PC:从一个普通 Electron 项目到鸿蒙可运行项目:vmd-master 适配实战全记录
华为·electron·harmonyos
nashane18 小时前
HarmonyOS 6学习:听书App被“误杀”?音频焦点与AudioSession共存避坑指南
学习·音视频·harmonyos