前言

我在做材料管理页的时候,最先不满意的是顶部那一排 Tab。Pura X Max 展开态里,顶部空间比较宽,全部材料、待处理、已完成、设置这几个入口都可以完整展示,甚至还能带上数量和说明文字。这个状态下,导航看起来信息很足,用户扫一眼就知道每个模块分别对应什么内容。

把窗口切到窄一些的尺寸后,顶部导航开始显得吃空间。原来四个 Tab 可以横向铺开,现在每个入口都被压成一小块,标题变短,数量角标挤在旁边,说明文字也会把导航撑高。页面内容区本来就变窄了,如果顶部继续占一大块,下面的列表能看到的内容就会少很多。

这个问题不只出现在材料管理页里,下面这些页面也经常会遇到类似情况:

  • 工作台顶部的模块切换
  • 任务列表里的状态筛选
  • 消息中心里的分类入口
  • 设置页里的分组导航
  • 审批、客户、订单这类业务后台的顶部筛选栏

这些页面有一个共同点,导航只是帮助用户切换模块,用户真正要处理的内容在下面。窗口宽的时候,导航可以多展示一点信息;窗口变窄以后,导航就要把一部分空间让给内容区。否则用户还没看到列表内容,顶部已经被标题、Tab、数量和说明占掉了一大截。

Pura X Max 的外屏是 5.4 英寸,内屏是 7.7 英寸,外屏分辨率为 1848 × 1264,内屏分辨率为 2584 × 1828,系统版本为 HarmonyOS 6.1。这个设备既有展开态的大空间,也会遇到外屏、分屏、悬浮窗这些窄窗口状态。顶部导航如果一直按照展开态来设计,窄窗口里最容易出问题的地方就是导航文字和内容区抢空间。

我这次用一个材料管理页来模拟这个场景。顶部有 4 个入口,宽屏下完整显示标题、数量和说明;窄屏下缩短文字,只保留短标题和数量;极窄窗口下不再横向铺开所有 Tab,只显示当前模块标题和一个切换按钮。这样处理以后,导航仍然能完成模块切换,内容区也能保留足够的阅读空间。

一、顶部空间会先被导航占掉

1.1 展开态里完整导航确实有用

在 Pura X Max 展开态里,顶部导航通常不会马上出问题。窗口宽度足够,4 个 Tab 同时显示完整标题、数量角标和简短说明,页面顶部还能保持一定留白。像材料管理页这种场景,用户可以直接看到“全部材料”“待处理”“已完成”“设置”,也能从数量上判断当前模块里有多少内容。

这种完整导航在宽屏下是有价值的。用户不需要点击更多按钮,也不需要猜某个短词代表什么业务。尤其是工作台、任务看板、消息中心这类页面,顶部导航不只是入口,也承担一点状态提示的作用。比如“待处理 7 项”比单独一个“待办”更能让用户判断当前页面有没有需要马上处理的内容。

示例里的展开态 Tab 会保留完整标题、数量和描述。

@Builder
private ExpandedTabBar() {
  Row({ space: 10 }) {
    ForEach(this.tabs, (item: TabItem) => {
      Column({ space: 5 }) {
        // 宽窗口下保留完整标题、数量和说明
      }
    }, (item: TabItem) => item.id.toString())
  }
}

这段结构放在展开态里没有问题,因为每个 Tab 都能分到足够宽度。标题不会被截断,数量角标也不会挤到内容文字上,说明文字还能作为辅助信息存在。到了窄窗口,真正要调整的不是 Tab 这个能力本身,而是这一排 Tab 还应该展示多少信息。

1.2 窄窗口里导航会挤掉内容

我把宽度切到 680vp 后,顶部导航虽然还能放下 4 个 Tab,但每个 Tab 的空间已经开始紧张。完整标题继续显示时,文字会被压缩,说明文字占用第二行,整个导航区的高度也会上来。页面顶部一高,下面的列表自然就往下退,一屏里能看到的材料卡片变少。

再窄一点,比如 390vp 左右,如果还横向摆 4 个完整 Tab,每个入口都只剩一点宽度。用户看不清标题,点击区域也会变得尴尬,当前模块反而不够突出。这个时候继续保留完整导航,页面并不会因此更容易使用。

我会把顶部导航拆成两个任务来看。一个任务是告诉用户当前在哪个模块,另一个任务是提供模块切换能力。窗口足够宽时,这两个任务可以由完整 TabBar 承担;窗口变窄后,完整 TabBar 需要收缩,至少要保证当前模块能被识别,切换入口还能找到。

二、导航信息按窗口宽度收起来

2.1 expanded 保留完整导航

宽窗口下,我会保留完整导航。这个状态适合 Pura X Max 展开态,或者任何足够宽的窗口。4 个 Tab 都显示完整标题、数量和说明,用户可以直接理解每个模块的含义,也能从数量上获得一些当前状态。

示例里用 expandedWidth 控制这个状态。

private readonly expandedWidth: number = 860;

860vp 不是固定标准。真实项目里要看 Tab 数量、文字长度、是否带数量角标,以及页面顶部是否还有搜索、筛选、更多按钮。如果 Tab 名称比较长,或者顶部还要放搜索入口,这个值就要适当提高。

我在真实项目里会先把顶部导航当成一个会占用空间的区域来处理。展开态里它可以展示更多信息,到了窄窗口里,它要把一部分空间还给内容区。这个判断如果不提前做,后面就容易反复通过缩字号、压 padding、减少间距来硬撑,最后导航和内容都会变得不好读。

2.2 compact 保留短标题和数量

窗口进入中等宽度时,我不会马上把 Tab 全部收起来。这个时候 4 个入口仍然可以横向展示,但文字要缩短,说明文字先去掉,只保留短标题和数量。

示例里 compact 的门槛是:

private readonly compactWidth: number = 620;

在这个区间里,“全部材料”会变成“全部”,“待处理”会变成“待办”,“已完成”会变成“完成”。短标题保留了切换能力,数量也继续提供状态提示,但导航高度和横向占用都减少了。

@Builder
private CompactTabBar() {
  Row({ space: 8 }) {
    ForEach(this.tabs, (item: TabItem) => {
      Column({ space: 4 }) {
        Text(item.shortTitle)
        Text(item.count.toString())
      }
    }, (item: TabItem) => item.id.toString())
  }
}

这里我没有直接把 compact 做成一个更多按钮。中等宽度下,用户仍然可以承受 4 个短入口。把所有入口都收起来,切换反而会多一步。顶部导航的收缩要分层,窗口稍微窄一点时可以先减文字,再去掉说明,最后才收成当前模块和切换按钮。

这个思路放到真实项目里也比较好用。每个 Tab 预先准备完整标题和短标题,宽窗口用完整标题,窄窗口用短标题,不需要运行时临时截字符串。临时截字符串容易出现语义不完整,比如“待处理事项”被截成“待处理事”,看起来就不太像一个稳定的入口。

2.3 minimal 显示当前模块和切换入口

窗口继续缩小以后,4 个 Tab 再横向排列就不合适了。这个时候我会让导航进入 minimal,只显示当前模块标题、当前数量和一个切换按钮。

@Builder
private MinimalTabBar() {
  Row({ space: 10 }) {
    Column({ space: 3 }) {
      Text(this.getCurrentTab().title)
      Text('共 ' + this.getCurrentTab().count.toString() + ' 项')
    }

    Button('切换')
  }
}

这个状态适合极窄分屏、悬浮窗或者临时小窗口。用户仍然知道自己在哪个模块,也能通过“切换”进入下一个模块。它没有完整 TabBar 直观,但在小窗口里,它能给内容区留下更多空间。

这里还有一个取舍。极窄窗口里不适合继续展示 4 个入口,但当前模块不能丢。用户至少要知道自己现在看的是“全部材料”还是“待处理”。如果只剩一个“更多”按钮,当前状态会变弱;如果保留当前模块标题和数量,用户就不至于迷路。

三、导航切换收在一个入口里

3.1 宽度判断不要散在各处

真正写到页面里,我不想让每个导航组件自己判断窗口宽度。这样做短期能跑,后面一旦断点变化,修改范围会很散。示例里把布局状态集中到 getLayoutMode()

private getLayoutMode(): string {
  const width = this.getEffectiveWidth();

  if (width >= this.expandedWidth) {
    return 'expanded';
  }

  if (width >= this.compactWidth) {
    return 'compact';
  }

  return 'minimal';
}

这个函数只做一件事,根据当前窗口宽度返回导航状态。后面的页面结构都消费这个结果。这样写的好处是断点规则集中在一个地方,ExpandedTabBarCompactTabBarMinimalTabBar 不需要各自关心阈值。

真实项目里可以把字符串换成枚举,或者把断点抽到统一配置中。比如整个应用都使用同一套窗口状态,列表页、详情页、表单页、导航页都能共享。页面内部再根据自己的业务决定每个状态下显示什么内容。这样后面适配 Pura X Max、平板、2in1 时,不会每个页面各写一套断点。

3.2 TopNavigation 负责分发组件

示例里用 TopNavigation() 做统一入口。

@Builder
private TopNavigation() {
  if (this.isExpanded()) {
    this.ExpandedTabBar()
  } else if (this.isCompact()) {
    this.CompactTabBar()
  } else {
    this.MinimalTabBar()
  }
}

这个写法把三种导航形态收在一个地方。页面主体不需要知道当前具体用的是完整 TabBar、短 TabBar,还是当前模块加切换按钮。它只需要调用 TopNavigation()

我在真实项目里会保留这种结构。顶部导航是一个整体,不应该把 expanded、compact、minimal 分散到页面各处。以后如果产品上要调整极窄窗口里的交互方式,比如从“切换”按钮改成下拉菜单,也只需要改 MinimalTabBar()TopNavigation() 这部分,不会影响内容区。

四、实际运行效果

这个我提供了“宽屏”“窄屏”“极窄”几个演示按钮,方便在同一台模拟器里观察顶部导航怎么变化。真实项目里可以删掉这些按钮,页面直接跟随真实窗口宽度切换。

宽屏状态下,顶部完整显示 4 个 Tab。每个 Tab 都有标题、数量和说明,适合 Pura X Max 展开态或较宽窗口。用户可以直接看到每个模块的含义,不需要额外点击。

窄屏状态下,导航仍然保留 4 个入口,但文字会缩短,说明文字被收起。全部材料变成全部,待处理变成待办,每个 Tab 只保留短标题和数量。这个状态下,导航仍然能切换模块,但不会把顶部撑得过高。

极窄状态下,完整 TabBar 会收起。顶部只显示当前模块标题、数量和一个切换按钮。点击切换按钮会按顺序切换模块。这个状态适合悬浮窗或很窄的分屏窗口,页面先保留当前内容可读,再保留基础切换能力。

我把这三张图放在一起做对比,就能看到顶部导航到底从哪里开始影响内容区。宽屏那张图里,完整 Tab 放在顶部没有什么压力,标题、数量和说明都能展开,下面的列表也还能保留足够空间。到了窄屏那张图,说明文字先收起来以后,顶部高度明显少了一截,但 4 个入口还在,用户仍然能直接切模块。再看极窄那张图,如果继续把 4 个 Tab 横向挤在一起,下面的内容区会被压得更厉害,所以我把它收成当前模块标题和一个切换按钮。

这个处理顺序基本就是我在项目里会采用的做法。先不要急着把导航全部藏起来,能保留短标题的时候就保留短标题;短标题也开始挤的时候,再收成当前模块。这样窗口从宽到窄变化时,用户不会突然失去导航入口,也不会因为顶部塞满 Tab,看不到下面真正要处理的材料列表。

五、放到真实项目如何操作

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 Tab 数量多时要换策略

这个示例里只有 4 个 Tab,所以 compact 状态还能保留 4 个短标题。真实项目里如果模块超过 5 个,我不会继续把所有入口都横向铺开。窄窗口里横向摆太多入口,用户反而更难点到目标模块。

这种情况下,可以考虑把高频模块留在顶部,把低频模块放到更多菜单里。比如首页、任务、消息可以保留;设置、归档、规则、历史记录放到更多入口里。这样顶部导航仍然承担主要切换职责,但不会把所有模块都挤在一行。

我这里会再看一个实际指标:用户切换某个模块的频率。高频入口才值得留在顶部,低频入口可以进入更多菜单。导航不是越完整越好,尤其是窄窗口里,把所有入口都展示出来,往往会让每个入口都变得不够好点。

5.3 minimal 里不要藏掉当前状态

极窄窗口下,虽然完整 TabBar 收起了,但当前模块标题和数量不能消失。用户至少要知道自己当前在哪个模块,以及这个模块里有多少内容。示例里的 MinimalTabBar() 保留了当前模块标题、数量和切换按钮,就是为了让用户不迷路。

Text(this.getCurrentTab().title)

Text('共 ' + this.getCurrentTab().count.toString() + ' 项')

Button('切换')

真实项目里也可以把“切换”按钮换成下拉菜单、更多按钮或者底部弹层。具体用哪种交互,要看模块数量和用户切换频率。如果只有 3 到 4 个模块,顺序切换还能接受;如果模块很多,就要给用户一个明确的菜单列表。

这个地方也要避免另一个极端。minimal 状态不能只剩一个图标按钮。用户看到一个按钮,但不知道自己在哪个模块,也不知道点了会切到哪里,操作会变得很没底。当前模块标题和数量保留下来,小窗口里的导航才算还能交代当前状态。

总结

Pura X Max 的顶部导航适配,主要处理的是窗口缩窄后的信息取舍。宽屏下完整显示 Tab 标题、数量和说明,用户可以直接理解每个模块;窄屏下缩短标题并收起说明,保留切换入口和数量;极窄窗口下只显示当前模块和切换按钮,把顶部空间让给内容区。

我处理这类页面时,会先给每个 Tab 准备完整标题、短标题和数量,再决定不同窗口宽度下展示哪一层信息。导航仍然承担切换职责,但它不能在小窗口里抢走太多内容空间。尤其是悬浮窗和分屏窗口,用户更关心当前内容是否能看、当前模块是否能切,而不是顶部导航是否展示得足够完整。

附:完整代码

interface TabItem {
  id: number;
  title: string;
  shortTitle: string;
  desc: string;
  count: number;
}

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

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

  // 演示宽度,只用于在同一个模拟器里观察 expanded / compact / minimal
  @State private previewWidth: number = 0;

  // 当前选中的导航模块。窗口变窄以后,minimal 状态仍然要保留当前模块
  @State private selectedTab: number = 0;

  // compact 以下进入当前模块模式,expanded 以上显示完整导航
  private readonly compactWidth: number = 620;
  private readonly expandedWidth: number = 860;

  private readonly tabs: TabItem[] = [
    {
      id: 0,
      title: '全部材料',
      shortTitle: '全部',
      desc: '查看所有整理记录',
      count: 28
    },
    {
      id: 1,
      title: '待处理',
      shortTitle: '待办',
      desc: '需要继续确认的内容',
      count: 7
    },
    {
      id: 2,
      title: '已完成',
      shortTitle: '完成',
      desc: '已经保存或归档',
      count: 16
    },
    {
      id: 3,
      title: '设置',
      shortTitle: '设置',
      desc: '处理规则和提醒偏好',
      count: 4
    }
  ];

  private readonly materials: MaterialItem[] = [
    {
      id: 1,
      title: '社区物业缴费提醒',
      status: '待处理',
      source: '拍照整理',
      time: '09:20',
      summary: '识别到缴费截止日期、金额明细和办理地点。'
    },
    {
      id: 2,
      title: 'Pura X Max 适配会议纪要',
      status: '待处理',
      source: '语音转写',
      time: '10:45',
      summary: '整理出顶部导航、分屏窗口和横屏适配问题。'
    },
    {
      id: 3,
      title: '活动报名确认单',
      status: '已完成',
      source: '相册导入',
      time: '11:30',
      summary: '提取到报名人、联系方式、活动时间和签到地址。'
    },
    {
      id: 4,
      title: '客户需求变更记录',
      status: '待处理',
      source: '文本整理',
      time: '13:10',
      summary: '本次变更涉及首页布局、权限配置和通知策略。'
    },
    {
      id: 5,
      title: '门诊复查预约提示',
      status: '已完成',
      source: '拍照整理',
      time: '16:40',
      summary: '提取到复查时间、科室、楼层和注意事项。'
    }
  ];

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

    return this.pageWidth;
  }

  // 把导航状态集中在一个函数里,避免每个导航组件各自判断宽度
  private getLayoutMode(): string {
    const width = this.getEffectiveWidth();

    if (width >= this.expandedWidth) {
      return 'expanded';
    }

    if (width >= this.compactWidth) {
      return 'compact';
    }

    return 'minimal';
  }

  private isExpanded(): boolean {
    return this.getLayoutMode() === 'expanded';
  }

  private isCompact(): boolean {
    return this.getLayoutMode() === 'compact';
  }

  private isMinimal(): boolean {
    return this.getLayoutMode() === 'minimal';
  }

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

    return '100%';
  }

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

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

    return 12;
  }

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

    if (this.isCompact()) {
      return 24;
    }

    return 20;
  }

  private getModeText(): string {
    if (this.isExpanded()) {
      return 'expanded · 完整导航';
    }

    if (this.isCompact()) {
      return 'compact · 简化导航';
    }

    return 'minimal · 当前模块';
  }

  private getModeDesc(): string {
    if (this.isExpanded()) {
      return '宽窗口下展示完整标题、数量和说明。';
    }

    if (this.isCompact()) {
      return '窄窗口下显示短标题和数量。';
    }

    return '极窄窗口下显示当前模块和切换按钮。';
  }

  private getCurrentTab(): TabItem {
    const found = this.tabs.find((item: TabItem) => item.id === this.selectedTab);
    return found ? found : this.tabs[0];
  }

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

  private switchToNextTab() {
    const next = this.selectedTab + 1;
    this.selectedTab = next >= this.tabs.length ? 0 : next;
  }

  private getVisibleMaterials(): MaterialItem[] {
    if (this.selectedTab === 1) {
      return this.materials.filter((item: MaterialItem) => item.status === '待处理');
    }

    if (this.selectedTab === 2) {
      return this.materials.filter((item: MaterialItem) => item.status === '已完成');
    }

    if (this.selectedTab === 3) {
      return [];
    }

    return this.materials;
  }

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

    return '#276749';
  }

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

    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 HeaderPanel() {
    Column({ space: this.isMinimal() ? 8 : 10 }) {
      Row() {
        Column({ space: 4 }) {
          Text('顶部导航在窄窗口下如何简化')
            .fontSize(this.getTitleSize())
            .fontWeight(FontWeight.Bold)
            .fontColor('#111827')
            .maxLines(this.isMinimal() ? 1 : 2)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          Text(this.getModeText())
            .fontSize(14)
            .fontColor('#2F8F83')
        }
        .layoutWeight(1)

        if (!this.isMinimal()) {
          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%')

      if (!this.isMinimal()) {
        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('窄屏', 680)
          this.PreviewButton('极窄', 390)
        }
        .width('100%')
      } else {
        Row({ space: 6 }) {
          this.PreviewButton('自动', 0)
          this.PreviewButton('宽屏', 960)
          this.PreviewButton('极窄', 390)
        }
        .width('100%')
      }
    }
    .width('100%')
  }

  @Builder
  private ExpandedTabBar() {
    Row({ space: 10 }) {
      ForEach(this.tabs, (item: TabItem) => {
        Column({ space: 5 }) {
          Row() {
            Text(item.title)
              .fontSize(15)
              .fontWeight(FontWeight.Medium)
              .fontColor(this.selectedTab === item.id ? '#FFFFFF' : '#111827')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Blank()

            Text(item.count.toString())
              .fontSize(12)
              .fontColor(this.selectedTab === item.id ? '#FFFFFF' : '#2F8F83')
              .padding({ left: 7, right: 7, top: 3, bottom: 3 })
              .backgroundColor(this.selectedTab === item.id ? '#33FFFFFF' : '#E6F4F1')
              .borderRadius(999)
          }
          .width('100%')

          Text(item.desc)
            .fontSize(12)
            .fontColor(this.selectedTab === item.id ? '#DFF5F1' : '#6B7280')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .layoutWeight(1)
        .padding(12)
        .backgroundColor(this.selectedTab === item.id ? '#2F8F83' : '#FFFFFF')
        .borderRadius(18)
        .border({
          width: 1,
          color: this.selectedTab === item.id ? '#2F8F83' : '#E5E7EB'
        })
        .onClick(() => {
          this.selectedTab = item.id;
        })
      }, (item: TabItem) => item.id.toString())
    }
    .width('100%')
  }

  @Builder
  private CompactTabBar() {
    Row({ space: 8 }) {
      ForEach(this.tabs, (item: TabItem) => {
        Column({ space: 4 }) {
          Text(item.shortTitle)
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor(this.selectedTab === item.id ? '#FFFFFF' : '#111827')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          Text(item.count.toString())
            .fontSize(11)
            .fontColor(this.selectedTab === item.id ? '#FFFFFF' : '#6B7280')
        }
        .layoutWeight(1)
        .padding({ top: 9, bottom: 9, left: 4, right: 4 })
        .backgroundColor(this.selectedTab === item.id ? '#2F8F83' : '#FFFFFF')
        .borderRadius(16)
        .border({
          width: 1,
          color: this.selectedTab === item.id ? '#2F8F83' : '#E5E7EB'
        })
        .onClick(() => {
          this.selectedTab = item.id;
        })
      }, (item: TabItem) => item.id.toString())
    }
    .width('100%')
  }

  @Builder
  private MinimalTabBar() {
    Row({ space: 10 }) {
      Column({ space: 3 }) {
        Text(this.getCurrentTab().title)
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor('#111827')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text('共 ' + this.getCurrentTab().count.toString() + ' 项')
          .fontSize(12)
          .fontColor('#6B7280')
      }
      .layoutWeight(1)

      Button('切换')
        .fontSize(13)
        .fontColor('#FFFFFF')
        .height(34)
        .width(72)
        .backgroundColor('#2F8F83')
        .borderRadius(17)
        .onClick(() => {
          this.switchToNextTab();
        })
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
    .shadow({
      radius: 8,
      color: '#10000000',
      offsetX: 0,
      offsetY: 4
    })
  }

  @Builder
  private TopNavigation() {
    if (this.isExpanded()) {
      this.ExpandedTabBar()
    } else if (this.isCompact()) {
      this.CompactTabBar()
    } else {
      this.MinimalTabBar()
    }
  }

  @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 MaterialCard(item: MaterialItem) {
    Column({ space: this.isMinimal() ? 8 : 10 }) {
      Row({ space: 8 }) {
        this.StatusPill(item.status)

        Blank()

        if (!this.isMinimal()) {
          Text(item.time)
            .fontSize(12)
            .fontColor('#6B7280')
        }
      }
      .width('100%')

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

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

        Text(item.source)
          .fontSize(12)
          .fontColor('#4B5563')
      }
    }
    .width('100%')
    .padding(this.isMinimal() ? 12 : 15)
    .backgroundColor('#FFFFFF')
    .borderRadius(this.isMinimal() ? 16 : 18)
    .border({
      width: 1,
      color: '#E5E7EB'
    })
  }

  @Builder
  private SettingsPanel() {
    Column({ space: 12 }) {
      Text('设置')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#111827')

      Text('这里模拟当前模块切换到设置后的内容区域。窄窗口下顶部导航不再完整铺开,内容区可以保留更多可读空间。')
        .fontSize(14)
        .fontColor('#6B7280')
        .lineHeight(22)

      Button('打开整理规则')
        .fontSize(15)
        .fontColor('#FFFFFF')
        .height(42)
        .width('100%')
        .backgroundColor('#2F8F83')
        .borderRadius(21)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(20)
  }

  @Builder
  private ContentArea() {
    Scroll() {
      Column({ space: 12 }) {
        if (this.selectedTab === 3) {
          this.SettingsPanel()
        } else {
          ForEach(this.getVisibleMaterials(), (item: MaterialItem) => {
            this.MaterialCard(item)
          }, (item: MaterialItem) => item.id.toString())

          if (this.getVisibleMaterials().length === 0) {
            Text('暂无记录')
              .fontSize(15)
              .fontColor('#6B7280')
              .width('100%')
              .textAlign(TextAlign.Center)
              .padding(24)
              .backgroundColor('#FFFFFF')
              .borderRadius(18)
          }
        }
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .layoutWeight(1)
    .width('100%')
    .edgeEffect(EdgeEffect.Spring)
  }

  build() {
    Column() {
      Column({ space: this.isMinimal() ? 10 : 16 }) {
        this.HeaderPanel()
        this.TopNavigation()
        this.ContentArea()
      }
      .width(this.getContentWidth())
      .height('100%')
      .padding({
        left: this.getPagePadding(),
        right: this.getPagePadding(),
        top: this.isMinimal() ? 10 : 18,
        bottom: this.isMinimal() ? 10 : 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;
      }
    })
  }
}
Logo

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

更多推荐