前言

我在做材料列表详情查看的时候,最早用的是居中弹窗。外屏下这个写法没什么违和感,用户点一条记录,页面弹出一个详情窗口,确认完再关掉,流程短,注意力也集中。手机空间本来就小,弹窗把当前任务单独拎出来,用户不会被列表里的其他内容干扰。

把同样的交互放到 Pura X Max 展开态里,我开始觉得不太对。列表区域明明还有很多空间,用户也能看到多条材料记录,但居中弹窗一出现,原来的列表和选中项都被遮住了。弹窗本身占据了屏幕中心,左右两边却空着不少区域。用户想对照原列表里的记录,或者继续切换另一条材料,就得先关掉弹窗,再回到列表里重新找。

这种情况在展开态里很常见,尤其是下面这些轻量任务:

  • 查看一条记录的补充说明
  • 编辑一两个字段
  • 临时筛选列表
  • 查看备注或处理建议
  • 确认一条识别结果
  • 给当前记录补充状态或标签

这些任务都有一个共同点,它们需要依附在当前页面上完成,不一定值得跳到完整详情页。小屏里可以用弹窗或底部面板承接;展开态里,右侧面板通常会更符合页面结构。左侧仍然保留列表或原内容,右侧承接当前详情和操作,用户不会丢掉自己是从哪条记录点进来的。

Pura X Max 在外屏、展开态、分屏和自由窗口之间切换时,页面可用宽度变化很大。弹层交互不能只按手机外屏的思路处理,窗口宽起来以后,页面有条件保留上下文,弹层也可以从遮住页面变成贴着页面补充信息。

这次我用一个材料列表页来模拟这个场景。点击“查看详情”后,窄窗口使用底部面板,展开态使用右侧面板。状态和数据仍然是一套,只是面板出现的位置跟着窗口宽度变化。这个处理方式比较适合详情补充、筛选条件、备注编辑这类轻量任务。

一、弹窗在展开态里会遮住上下文

1.1 外屏里弹窗可以集中注意力

外屏里用弹窗处理详情补充,很多时候是可以接受的。比如用户在一个窄屏列表里点开某条材料,弹出一个居中的详情卡片,或者从底部拉起一个面板,用户的注意力会集中到这条记录上。页面空间有限,原列表本来就无法和详情同时展开,弹窗相当于给当前任务临时开出一块区域。

我在手机外屏里也会经常这样写。比如编辑一个标题、确认一个提醒、查看一段识别结果,弹窗或底部面板能把任务收得比较干净。用户看完以后关掉弹层,回到原页面继续操作。这个模式在小屏里并不违和,反而能减少页面跳转。

在代码里,这类写法通常很简单。点一条记录,把 showPanel 设为 true,再把当前记录 id 存下来。

private openPanel(itemId: number) {
  this.selectedId = itemId;
  this.showPanel = true;
}

这个状态本身可以保留。真正要调整的是弹层在不同窗口宽度下的呈现方式。外屏可以从底部出来,展开态就没必要继续遮住页面中心。

1.2 展开态里弹窗会抢掉参照物

我把这个页面切到展开态后,再点查看详情,第一个感受就是弹窗挡住了列表。原来的材料列表还在背后,但用户已经看不到自己点的是哪条记录,也看不到上下几条记录之间的关系。对于只查看一条详情来说,这还勉强能用;如果用户需要连续切换记录,居中弹窗就开始影响操作。

展开态的价值之一,是能把原页面和补充内容同时放下。比如左侧继续保留列表,右侧显示详情补充。用户在查看详情时,还能看到列表里其他材料,也能确认当前记录的上下文。这个时候居中弹窗反而把展开态空间浪费掉了。

我会把这类弹层分成两种用途来区分。需要强打断、强确认的内容,比如删除确认、支付确认、危险操作提醒,仍然适合弹窗;只是查看详情、筛选条件、备注编辑、轻量补充信息,就更适合放到侧边面板里。它们不需要遮住整个页面,也不需要让用户离开当前上下文。

二、小屏继续用底部面板

2.1 窄窗口更适合聚焦处理

在外屏或较窄窗口里,我仍然会保留底部面板。原因很简单,窄窗口里很难同时放下列表和详情,用户点开一条记录时,页面优先让他处理当前内容。底部面板从屏幕下方出现,覆盖原页面的一部分,用户会自然把注意力放到当前记录上。

这类交互适合短任务。比如查看一条提醒、确认一段识别结果、保存一个处理建议。用户不需要保留大量上下文,只要知道当前处理对象是什么,以及下一步能点哪个按钮。

示例里,小屏下的底部面板只在 showPanel 为 true 且窗口没有达到展开态时出现。

if (this.showPanel && !this.isExpanded()) {
  this.BottomSheet()
}

这个判断看起来很简单,但它决定了小屏里的交互节奏。窄窗口不强行分栏,也不把详情塞在右侧,而是用底部面板集中当前任务。等窗口宽起来以后,同样的详情内容再换到右侧面板。

2.2 底部面板要控制高度

底部面板不能无限长。小屏里高度本来就有限,如果面板展开后把整个屏幕都占满,用户会感觉像跳进了另一个页面,但返回关系又没有完整页面那么清楚。

示例里,底部面板用了固定高度。

.height(430)

这个值不是固定标准。真实项目里要根据内容决定,外屏页面里可以略高一点,悬浮窗里要更克制。一般来说,底部面板适合放标题、摘要、少量元信息和一到两个按钮。如果内容继续增长,就要考虑进入完整详情页,而不是让底部面板一直加高。

我在项目里通常会把底部面板看作临时处理区,不会把完整详情全部塞进去。它可以承接当前动作,但不适合承担一个复杂流程。这样后续迁移到展开态侧边面板时,也能保持同一套信息层级。

三、展开态改成右侧面板

3.1 右侧面板保留原页面参照

展开态下,我更愿意把详情补充内容放到右侧。左侧仍然是列表,右侧面板显示当前记录的详情、建议和操作按钮。这样用户打开详情时,不会失去原页面参照,也能继续知道自己是从哪条记录点进来的。

示例里的判断从窗口宽度开始。

private readonly expandedWidth: number = 820;

private isExpanded(): boolean {
  return this.getEffectiveWidth() >= this.expandedWidth;
}

820vp 是这个示例里的门槛。真实项目里要看页面主体宽度、侧边面板宽度、左右 padding 和列表卡片宽度。比如右侧面板需要 360vp,左侧列表至少要保留 420vp,再加上间距和页面边距,阈值就不能设得太低。

大屏下显示侧边面板的判断也很简单。

if (this.showPanel && this.isExpanded()) {
  this.SidePanel()
}

我在真实项目里会把这个判断放在页面层,而不是让某个按钮组件自己决定弹出方式。按钮只负责打开详情,至于详情出现在底部还是右侧,交给页面根据窗口宽度处理。

3.2 面板宽度要给主页面留空间

示例里的右侧面板宽度是 360vp。

.width(360)

这个宽度适合展示标题、摘要、元信息、补充说明和两个按钮。它不会太窄,正文还可以阅读;也不会太宽,左侧列表仍然能保留足够空间。如果面板内容更少,可以降到 320vp;如果需要展示表单字段,可以增加到 400vp 左右。

这里我会特别关注左侧列表。侧边面板出现以后,左侧仍然应该能看清列表标题、选中态和至少几条记录。如果右侧面板过宽,左侧列表被挤得只剩窄条,那就和居中弹窗一样失去了保留上下文的意义。

四、用 Stack 还原两种形态

4.1 页面层控制遮罩和面板

这里我没有直接调用系统弹窗 API,而是在页面里用 Stack 叠出遮罩、底部面板和右侧面板。这样做的好处是方便验证布局逻辑,也方便在同一个页面里根据窗口宽度切换不同面板形态。

核心结构放在 build() 里。

if (this.showPanel) {
  Column()
    .width('100%')
    .height('100%')
    .backgroundColor(this.isExpanded() ? '#00000000' : '#66000000')

  if (this.isExpanded()) {
    this.SidePanel()
  } else {
    this.BottomSheet()
  }
}

小屏下,遮罩是半透明黑色,点击遮罩可以关闭面板。这个交互更接近普通弹窗,用户知道当前任务是临时打开的。展开态下,遮罩保持透明,面板出现在右侧,左侧列表仍然可见。关闭动作放在面板内部,避免用户误触左侧列表时把面板关掉。

这两个细节很容易被忽略。很多时候我们只处理面板位置,却忘了遮罩也要跟着变化。小屏需要遮罩帮助用户聚焦,大屏更需要保留页面上下文。遮罩颜色、关闭方式、面板位置,其实都应该和窗口状态一起调整。

4.2 详情内容复用同一份

这里底部面板和右侧面板都使用同一个 DetailContent()。也就是说,详情内容没有拆成两份,只是外层容器不同。

@Builder
private DetailContent(item: MaterialItem) {
  Column({ space: 16 }) {
    // 标题、摘要、补充说明、处理建议和按钮
  }
}

这样写的好处是,后面改详情字段时,不需要同时改底部面板和右侧面板两套内容。小屏和大屏的差异主要体现在外层容器:小屏是从底部出现,大屏是贴右侧出现。详情内部的字段结构可以保持一致,再根据宽度做少量字号或行高调整。

真实项目里也建议这样处理。弹层形态可以有两种,内容尽量保持一份。否则后面加字段、改文案、调整按钮状态时,很容易出现小屏和大屏不一致。

五、实际运行效果

这里顶部有外屏和展开态两个演示按钮,方便在同一台模拟器里观察面板形态。真实项目里可以删掉这些按钮,页面直接根据真实窗口宽度判断。

外屏状态下,点击任意一条记录的查看详情,详情会从底部弹出,背景有一层半透明遮罩。这个状态适合窄窗口,用户的注意力集中在当前记录上,底部面板也更贴近小屏操作习惯。

展开态状态下,再点击查看详情,详情会出现在右侧,左侧列表不会被遮住。选中的列表项会保留高亮,用户能看到自己打开的是哪条记录,也可以继续对照列表里的其他材料。

六、如何迁移到实际项目

6.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 来说,同一台设备可能处在外屏、展开态、分屏和自由窗口里,面板形态要看当前窗口给了多少空间。

6.2 不是所有弹窗都适合改成侧边面板

侧边面板适合轻量补充任务,比如查看详情、筛选条件、备注编辑、状态确认。它的价值在于保留原页面上下文,让用户不离开列表,也能完成一小段操作。

如果是删除确认、支付确认、权限授权这类需要强提醒的动作,我仍然会用弹窗。它们本来就需要打断用户,让用户明确确认当前操作。侧边面板太轻,反而不适合这类高风险动作。

如果是多步骤表单、长文编辑、图片裁剪、复杂审批流程,我也不会放在侧边面板里。右侧面板宽度有限,复杂流程放进去会让用户一直滚动,还容易丢失表单上下文。这类任务应该进入完整页面,或者使用更大的编辑页面承接。

6.3 面板状态要和选中项保持一致

示例里用了 selectedId 保存当前选中记录,用 showPanel 控制面板是否显示。点击不同记录时,面板内容会跟着更新。

private openPanel(itemId: number) {
  this.selectedId = itemId;
  this.showPanel = true;
}

真实项目里也要留意这个状态关系。用户在展开态里点击列表 A,右侧面板显示 A 的详情;继续点击列表 B,右侧面板应该切到 B,而不是重新弹出一个新的弹窗。这样列表和面板之间的关系才是连续的。

我会把这类状态放在页面层,而不是放在单个列表卡片里。列表卡片负责触发打开动作,页面负责保存选中项和面板状态。这样底部面板和右侧面板都能复用同一份状态,不会因为窗口宽度变化导致当前详情丢失。

总结

Pura X Max 展开态里,弹窗继续放在屏幕中间,很多时候会把原页面关系打断。外屏空间小,底部面板可以让用户先处理当前记录;展开态空间变宽以后,详情补充、筛选条件、备注编辑这类内容放到右侧,左侧列表还能留在原位,用户知道自己刚才点的是哪条记录,也能继续对照上下几条材料。

我后面处理这类弹层时,会先看它承担的任务:

  • 如果只是查看详情、补充说明、备注编辑、筛选条件,右侧面板更适合展开态。
  • 如果是删除、支付、授权这类强确认动作,居中弹窗仍然更合适,因为它需要让用户停下来确认。
  • 如果是多步骤表单、长文编辑、图片裁剪这类复杂流程,应该进入完整页面,不适合塞进侧边面板。
  • 如果只是外屏上的短任务,比如看一条记录、点一下保存、稍后处理,底部面板已经够用。

弹层的位置要看当前窗口能不能保留原页面参照。窗口窄的时候,先让用户集中处理当前记录;窗口宽的时候,就不要急着遮住列表,把补充内容放到右侧,让原页面和详情内容同时留在视野里。这样处理以后,弹层不再只是一个固定样式,而是会根据任务轻重和窗口宽度换一种呈现方式。

附:完整代码

interface MaterialItem {
  id: number;
  title: string;
  status: string;
  source: string;
  time: string;
  tag: string;
  summary: string;
  detail: string;
  suggestion: string;
}

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

  // 演示宽度,只用于在同一个模拟器里观察外屏和展开态
  @State private previewWidth: number = 0;

  // 当前选中项。底部面板和右侧面板都读取这个状态
  @State private selectedId: number = 1;

  // 面板是否打开。窗口宽度变化时,面板位置会跟着切换
  @State private showPanel: boolean = false;

  // 模拟保存次数,用来观察面板切换后操作状态是否保留
  @State private saveCount: number = 0;

  private readonly expandedWidth: number = 820;

  private readonly materials: MaterialItem[] = [
    {
      id: 1,
      title: '社区物业缴费提醒',
      status: '待处理',
      source: '拍照整理',
      time: '09:20',
      tag: '通知',
      summary: '识别到缴费截止日期、金额明细和办理地点。',
      detail: '这条记录来自一张社区物业缴费通知。内容包含缴费周期、应缴金额、截止日期和办理地点。小屏下适合通过底部面板快速查看,大屏下可以用右侧面板保留列表上下文。',
      suggestion: '保存为待办提醒,并在截止日期前一天提醒。'
    },
    {
      id: 2,
      title: 'Pura X Max 适配会议纪要',
      status: '待确认',
      source: '语音转写',
      time: '10:45',
      tag: '会议',
      summary: '整理出弹窗、侧边面板、分屏窗口和横屏结构几类问题。',
      detail: '会议纪要类记录经常需要对照多个条目。展开态下右侧详情面板能减少页面跳转,列表仍然保留在原位置,切换记录也更方便。',
      suggestion: '确认适配任务,并同步到开发清单。'
    },
    {
      id: 3,
      title: '客户需求变更记录',
      status: '待处理',
      source: '文本整理',
      time: '13:10',
      tag: '项目',
      summary: '本次变更涉及首页布局、权限配置和消息提醒。',
      detail: '需求变更类记录适合在右侧面板里查看补充信息。主页面保留列表,右侧承接详情、处理建议和操作按钮。',
      suggestion: '同步项目负责人,并拆分到研发排期。'
    },
    {
      id: 4,
      title: '活动报名确认单',
      status: '已保存',
      source: '相册导入',
      time: '15:25',
      tag: '表单',
      summary: '提取到报名人、联系方式、活动时间和签到地址。',
      detail: '报名确认类材料通常只是补充查看,不一定需要进入完整详情页。小屏弹出底部面板,宽屏使用侧边面板即可。',
      suggestion: '保存记录,并在活动前一天提醒。'
    },
    {
      id: 5,
      title: '门诊复查预约提示',
      status: '已整理',
      source: '拍照整理',
      time: '16:40',
      tag: '提醒',
      summary: '提取到复查时间、科室、楼层和注意事项。',
      detail: '提醒类信息适合轻量处理。侧边面板可以承接确认、保存、稍后处理等动作,避免把用户带到另一个页面。',
      suggestion: '加入日程提醒,并保留原始记录。'
    }
  ];

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

    return this.pageWidth;
  }

  private isExpanded(): boolean {
    return this.getEffectiveWidth() >= this.expandedWidth;
  }

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

    return '100%';
  }

  private getPagePadding(): number {
    return this.isExpanded() ? 24 : 16;
  }

  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 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;
    this.showPanel = false;
  }

  private openPanel(itemId: number) {
    this.selectedId = itemId;
    this.showPanel = true;
  }

  private closePanel() {
    this.showPanel = false;
  }

  private save() {
    this.saveCount += 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() {
        Column({ space: 4 }) {
          Text('大屏弹窗改成侧边面板')
            .fontSize(this.getTitleSize())
            .fontWeight(FontWeight.Bold)
            .fontColor('#111827')

          Text(this.getModeText())
            .fontSize(14)
            .fontColor('#2F8F83')
        }
        .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)

      Row({ space: 8 }) {
        this.PreviewButton('自动', 0)
        this.PreviewButton('外屏', 430)
        this.PreviewButton('展开态', 960)
      }
      .width('100%')
    }
    .width('100%')
  }

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

        Blank()

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

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

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

      Row({ space: 10 }) {
        Text(item.source)
          .fontSize(12)
          .fontColor('#4B5563')

        Blank()

        Button('查看详情')
          .fontSize(13)
          .fontColor('#FFFFFF')
          .height(34)
          .padding({ left: 12, right: 12 })
          .backgroundColor('#2F8F83')
          .borderRadius(17)
          .onClick(() => {
            this.openPanel(item.id);
          })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(this.selectedId === item.id && this.showPanel ? '#EEF7F5' : '#FFFFFF')
    .borderRadius(20)
    .border({
      width: this.selectedId === item.id && this.showPanel ? 1.5 : 1,
      color: this.selectedId === item.id && this.showPanel ? '#2F8F83' : '#E5E7EB'
    })
    .shadow({
      radius: this.selectedId === item.id && this.showPanel ? 12 : 8,
      color: '#12000000',
      offsetX: 0,
      offsetY: 4
    })
  }

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

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

        Blank()

        Text('关闭')
          .fontSize(13)
          .fontColor('#6B7280')
          .padding({ left: 10, right: 10, top: 6, bottom: 6 })
          .backgroundColor('#F3F4F6')
          .borderRadius(999)
          .onClick(() => {
            this.closePanel();
          })
      }
      .width('100%')

      Column({ space: 8 }) {
        Text(item.title)
          .fontSize(this.isExpanded() ? 24 : 21)
          .fontWeight(FontWeight.Bold)
          .fontColor('#111827')
          .lineHeight(this.isExpanded() ? 31 : 28)

        Text(item.summary)
          .fontSize(14)
          .fontColor('#4B5563')
          .lineHeight(22)
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)

      Row({ space: 8 }) {
        this.MetaPill(item.source)
        this.MetaPill(item.time)
        this.MetaPill(item.tag)
      }
      .width('100%')

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

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

      Column({ space: 8 }) {
        Text('处理建议')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#111827')

        Text(item.suggestion)
          .fontSize(14)
          .fontColor('#4B5563')
          .lineHeight(23)
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#F3F8F7')
      .borderRadius(16)

      Text('已保存 ' + this.saveCount.toString() + ' 次')
        .fontSize(13)
        .fontColor('#6B7280')

      Button('保存处理结果')
        .fontSize(15)
        .fontColor('#FFFFFF')
        .height(44)
        .width('100%')
        .backgroundColor('#2F8F83')
        .borderRadius(22)
        .onClick(() => {
          this.save();
        })

      Button('稍后处理')
        .fontSize(15)
        .fontColor('#2F8F83')
        .height(42)
        .width('100%')
        .backgroundColor('#E6F4F1')
        .borderRadius(21)

      if (this.isExpanded()) {
        Text('展开态下,右侧面板不会遮住左侧列表,用户可以继续保留原页面参照。')
          .fontSize(13)
          .fontColor('#6B7280')
          .lineHeight(20)
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  private SidePanel() {
    Row() {
      Blank()

      Column() {
        this.DetailContent(this.getSelectedItem())
      }
      .width(360)
      .height('100%')
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius({
        topLeft: 24,
        topRight: 0,
        bottomLeft: 24,
        bottomRight: 0
      })
      .shadow({
        radius: 16,
        color: '#18000000',
        offsetX: -4,
        offsetY: 0
      })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  private BottomSheet() {
    Column() {
      Blank()

      Column() {
        this.DetailContent(this.getSelectedItem())
      }
      .width('100%')
      .height(430)
      .padding(18)
      .backgroundColor('#FFFFFF')
      .borderRadius({
        topLeft: 24,
        topRight: 24,
        bottomLeft: 0,
        bottomRight: 0
      })
      .shadow({
        radius: 16,
        color: '#18000000',
        offsetX: 0,
        offsetY: -4
      })
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack() {
      Column() {
        Column({ space: 16 }) {
          this.HeaderPanel()
          this.ListArea()
        }
        .width(this.getContentWidth())
        .height('100%')
        .padding({
          left: this.getPagePadding(),
          right: this.getPagePadding(),
          top: 18,
          bottom: 16
        })
      }
      .width('100%')
      .height('100%')
      .alignItems(HorizontalAlign.Center)

      if (this.showPanel) {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor(this.isExpanded() ? '#00000000' : '#66000000')
          .onClick(() => {
            if (!this.isExpanded()) {
              this.closePanel();
            }
          })

        if (this.isExpanded()) {
          this.SidePanel()
        } else {
          this.BottomSheet()
        }
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F6F7F9')
    .onAreaChange((_: Area, newValue: Area) => {
      const width = Number(newValue.width);
      if (!Number.isNaN(width) && width > 0) {
        this.pageWidth = width;
      }
    })
  }
}
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐