鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 19:设置页在 Pura X Max 上改成分组布局

前言

设置页很容易被写成一条长列表。账号、通知、权限、缓存、关于应用,全都从上到下排。手机外屏上这么写没有太大问题,用户打开设置页以后,从顶部一路往下找,看到需要的设置项就点进去。设置项数量不多时,单列列表甚至是最省事的写法。

我把同样的设置页放到 Pura X Max 展开态里看时,第一眼注意到的是设置项被拉得太长。左侧是设置标题,右侧是开关、状态或者跳转入口,中间被屏幕宽度拉开了一大段。用户看相机权限时,视线要从左侧标题扫到右侧状态;看版本号时,也要在一条很长的横向区域里找到对应值。单个设置项还能读,整页的分组关系却变弱了。

设置页和搜索页、列表页的使用方式不一样。搜索页会频繁调整条件,列表页主要用来浏览内容;设置页更像一个低频但要快速定位的功能入口集合。用户不一定每天打开设置页,但一旦打开,通常是带着明确目的进来的,比如改通知、看权限、清理缓存、查看版本、打开某个开关。这个时候,分类定位比单纯把列表拉宽更有价值。

这类设置页通常会包含几组内容。

  • 基础设置,比如显示方式、通知提醒、自动保存
  • 权限设置,比如相机、相册、麦克风、通知权限
  • 数据设置,比如缓存、同步、导入导出
  • 关于应用,比如版本号、隐私协议、用户协议、反馈入口

Pura X Max 展开态空间足够把设置页拆成左侧分类和右侧设置项,外屏和较窄窗口继续保留单列设置列表。鸿蒙里的全屏、分屏、自由窗口都会改变应用可用宽度,设置页如果只把手机端长列表拉宽,很快会遇到分类弱、行距远、定位慢这些问题。

我这次用一个设置页示例来验证这种改法。页面包含基础设置、权限设置、关于应用三个分类。小屏下仍然按分组从上到下展示;展开态下左侧显示分类卡片,右侧只展示当前分类下的设置项。这样用户在大屏里可以先选分类,再看具体设置,不需要在一条很长的设置列表里不断向下扫。

一、设置页不能只拉宽

1.1 外屏单列适合连续浏览

外屏下,设置页用单列结构是合理的。设置项按分组向下排列,用户进入页面以后,从顶部看到基础设置,再往下看到权限设置、关于应用。屏幕窄,单列列表能保证每个设置项都有足够的横向空间,标题、说明、右侧状态或开关都能放得下。

最常见的写法是这样的。

arkts 复制代码
Column() {
  this.SettingSection('基础设置')
  this.SettingSection('权限设置')
  this.SettingSection('关于应用')
}
.width('100%')

这个结构维护起来也简单。每个分组是一段列表,新增设置项时继续往对应分组里加。手机外屏上,用户滚动一下就能看到所有设置内容,页面也不会被左右拆开。

我以前做手机端设置页时,也会先用这种结构。设置页不是高频操作页,单列列表的可理解性很好。只要分组标题明确,设置项说明写得具体,小屏体验通常不会出大问题。这个时候没有必要为了大屏思维提前把页面拆复杂。

1.2 展开态单列会拉长信息路径

展开态里继续使用单列,问题会换一种方式出现。每一行设置项被拉得很宽,右侧开关或状态离左侧标题很远。用户看通知提醒时,标题在左,开关在右;看缓存清理时,说明在左,操作入口在右。单行看起来没有错,但整页的阅读路径被横向拉长了。

设置页在大屏里更需要分类定位。用户进来以后,应该先看到基础设置、权限设置、关于应用这些方向,再进入某一组具体设置。比如我想检查相机权限,就不该从通知提醒、自动保存、深色模式一路扫过去;我想看版本和隐私协议,也不需要穿过所有基础设置。

我会把大屏设置页拆成三个区域关系进行分析。

区域 小屏处理 展开态处理 主要目的
设置分类 作为分组标题嵌在列表中 固定在左侧 先定位方向
设置项列表 所有分组连续排列 只展示当前分类 减少纵向寻找
状态与操作 跟在每一行右侧 保留在右侧列表内 保持原有设置行为

这个表格能帮我避免一个误区。展开态不一定要展示更多设置项,它更应该把设置项的组织关系摆出来。左侧分类不是装饰,它承担的是入口定位;右侧设置项才是具体操作区域。

二、先拆分类,再拆设置项

2.1 分类要从标题变成入口

在单列设置页里,基础设置、权限设置、关于应用只是分组标题。它们帮助用户理解下面的设置项属于哪一类,但不会参与交互。到了展开态,这些分组标题可以升级成左侧分类导航。用户点击左侧分类,右侧切换对应设置项。

这个变化不只是把 UI 从上下结构改成左右结构,还牵涉到一个基础的状态设计。当前选中的设置分类要放在页面层,因为左侧分类和右侧设置项都要读取它。左侧需要知道哪个分类高亮,右侧需要知道展示哪组设置项。

示例里用 selectedGroupId 保存当前分类。

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

这个状态不能放在左侧分类组件里。左侧分类只是触发切换的入口,右侧内容也需要它。把状态放在页面层以后,外屏和展开态都能读同一份当前分类状态,窗口宽度变化时也不会重置。

这里其实是一个很常见的编程常识:当两个区域都依赖同一个状态时,状态应该提升到它们共同的父级。设置页左右分栏只是一个具体场景,列表详情、搜索筛选、图片预览里的左右区域也会遇到同样的问题。

2.2 设置项要用同一套数据

我不建议为基础设置、权限设置、关于应用分别写三套 UI。它们的业务含义不同,但展示结构很接近:标题、说明、右侧内容、类型和状态。这样就可以抽成统一的设置项数据,再根据 type 决定右侧展示开关、文本、按钮还是跳转状态。

设置项可以先按这样的结构理解。

设置项类型 页面表现 示例
switch 右侧显示开关 通知提醒、自动保存
link 右侧显示状态或入口 相机权限、相册权限
value 右侧显示文本值 版本号、缓存大小
action 右侧显示操作按钮 清理缓存、导入示例

不同类型的设置项,可以走同一个 SettingRow(),再根据 type 决定右侧展示什么。这个写法比每个分组手写一堆 Row 更适合维护。后面新增数据同步、订阅管理、隐私协议、实验功能这些内容时,也只是新增数据,不需要改布局结构。

这里还要留意设置项说明的长度。说明文字太长,小屏里会把行高撑得过高,展开态里也会让右侧列表变得松散。设置项说明只解释当前设置的影响就够了,完整帮助文档不要放进设置行。

三、分栏前先保住右侧

3.1 分类栏不能抢设置项宽度

设置页的左侧分类栏不需要太宽,但右侧设置项列表必须能读。展开态分栏如果只看窗口是否超过某个阈值,可能会出现左侧分类栏出现了,右侧设置项却被压得很窄。标题、说明、开关、状态挤在一行里,页面看起来进入了大屏结构,实际操作并没有变轻松。

我会先给左侧分类栏和右侧设置项列表分别留宽度。示例里左侧分类栏是 260vp,右侧设置项列表至少保留 560vp,中间间距是 16vp。进入双栏前,先计算这些区域是否真的放得下。

arkts 复制代码
private readonly groupPanelWidth: number = 260;
private readonly detailMinWidth: number = 560;
private readonly twoColumnGap: number = 16;

这些数字不用照搬。设置项比较短时,右侧 520vp 也可以;如果设置项说明更多,或者右侧同时出现开关、状态、按钮,右侧最小宽度就要提高。大屏适配不该看到宽度变大就马上分栏,分栏前要先确认主操作区域还能正常阅读和点击

3.2 可用宽度比屏幕宽度更有用

示例里的判断会先扣掉左右 padding,再计算左侧分类栏、右侧详情区和中间间距。

arkts 复制代码
private canUseSplitSettings(): boolean {
  const width = this.getEffectiveWidth();
  const availableWidth = width - this.getPagePadding() * 2;
  const requiredWidth = this.groupPanelWidth + this.twoColumnGap + this.detailMinWidth;

  return width >= this.expandedThreshold && availableWidth >= requiredWidth;
}

这里有个常见误区,很多布局判断会直接拿窗口宽度和阈值比较。这个写法短期能用,但只要页面加了左右 padding、卡片间距、侧栏宽度,就容易在中间尺寸出问题。设置页这种页面尤其容易被忽略,因为它看起来只是普通列表,实际右侧每一行都有标题、说明、开关或状态,不能被压得太窄。

我会把 canUseSplitSettings() 放在页面层。页面层负责判断采用单列还是双栏;左侧分类组件只负责分类展示;右侧设置项列表只负责当前分类的设置项。这样组件职责会更清楚,后面新增设置分类时,也不会影响断点逻辑。

四、实际运行结果

4.1 外屏先保留单列列表

这个示例会模拟一个设置页。小屏下,基础设置、权限设置、关于应用三个分组按顺序排列,用户从上往下滚动。展开态下,左侧显示三个设置分类,右侧显示当前分类下的设置项。点击左侧分类时,右侧内容切换。

小屏状态下,我会继续保留单列列表。原因很直接:设置项虽然多,但屏幕宽度有限,强行做左右结构会让两边都很窄。用户在小屏里从上往下浏览,不会因为多一次滚动就失去方向。只要分组标题足够清楚,单列结构可以继续使用。

4.2 展开态再把分类放左侧

展开态截图要看左侧分类和右侧设置项之间的关系。左侧分类高亮当前分类,右侧只显示当前分类下的设置项。这样用户不需要在长列表里继续寻找权限、关于应用或者缓存设置。

这个示例里还保留了几个开关状态,比如通知提醒、自动保存、深色模式。真实项目里,这些状态可以来自本地持久化配置;权限类设置则要来自系统权限状态;关于应用里的版本号、协议入口、反馈入口可以来自应用配置。

五、真实项目时怎么处理

5.1 分类和设置项最好来自配置

示例里的设置组和设置项写在页面里,是为了让代码可以直接运行。真实项目里,设置项通常会随着版本持续增加,比如订阅、数据同步、隐私、缓存、实验功能。继续把所有设置行写死在页面里,后面会越来越难维护。

我会把设置页拆成配置数据:

  • 分类配置负责标题、说明、图标和排序
  • 设置项配置负责标题、说明、类型、右侧文案、点击动作
  • 页面状态负责当前选中分类和开关状态
  • 具体业务逻辑交给对应服务处理

这样设置页的 UI 结构会更稳定。新增一个设置项时,优先改配置;新增一个业务动作时,再补对应处理函数。页面本身不需要因为每个设置项都去加一段重复代码。

5.2 权限设置要接真实状态

权限设置是设置页里比较特殊的一类。示例里用去开启、已授权这类文案模拟状态,真实项目里要接系统权限查询结果。相机、相册、麦克风、通知这些权限,用户可能在系统设置里改掉,回到应用后页面要能刷新状态。

这个地方不能只靠本地开关模拟。权限状态应该来自系统能力或应用启动后的检查结果,设置页只负责展示和跳转。比如用户点击相机权限,页面可以跳到授权引导或系统权限设置;用户返回后,再刷新当前权限状态。

5.3 关于应用单独成组

关于应用这类信息经常被随手放到设置页底部。手机上这么放可以接受,展开态里如果继续和基础设置混在一起,用户会在通知开关、自动保存、缓存清理这些设置项之间找版本号和协议入口。

我会把关于应用单独做成一个分类。版本号、隐私政策、用户协议、反馈入口、开发者信息,都放到同一个分类下。这样右侧区域展示时也更清楚,用户不会在一条很长的设置列表里找这些低频但重要的入口。

总结

设置页在 Pura X Max 展开态里,不一定要继续做成长单列。外屏下,单列设置列表适合连续浏览;展开态里,把分类放到左侧、设置项放到右侧,用户更容易先定位方向,再处理具体设置。

我后面处理设置页时,会先把内容按这几类拆开:

  • 基础设置放常用开关,比如通知、自动保存、深色模式。
  • 权限设置放相机、相册、麦克风、通知权限,并接真实权限状态。
  • 关于应用放版本号、协议、反馈和开发者信息。
  • 数据和缓存类设置可以单独成组,不要混在基础设置里。
  • 展开态是否分栏,要先确认右侧设置项区域还有足够宽度。

真实项目里,设置页会随着版本持续增长。分类和设置项最好配置化,页面层只负责选中分类、布局判断和状态分发。这样外屏单列、展开态双栏都能读同一套数据,后面新增设置项时,也不会把页面写成越来越长的一整段 UI。

附:完整代码

arkts 复制代码
interface SettingGroup {
  id: number;
  title: string;
  desc: string;
  badge: string;
}

interface SettingItem {
  id: number;
  groupId: number;
  title: string;
  desc: string;
  type: string;
  value: string;
  key: string;
}

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

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

  // 展开态左侧选中的设置分类
  @State private selectedGroupId: number = 1;

  // 模拟几个设置项状态,真实项目里可以来自持久化配置或系统权限查询
  @State private notifyEnabled: boolean = true;
  @State private autoSaveEnabled: boolean = true;
  @State private darkModeEnabled: boolean = false;
  @State private cacheClearedCount: number = 0;

  private readonly expandedThreshold: number = 860;
  private readonly groupPanelWidth: number = 260;
  private readonly detailMinWidth: number = 560;
  private readonly twoColumnGap: number = 16;

  private readonly groups: SettingGroup[] = [
    {
      id: 1,
      title: '基础设置',
      desc: '通知、显示和保存偏好',
      badge: '常用'
    },
    {
      id: 2,
      title: '权限设置',
      desc: '相机、相册和麦克风权限',
      badge: '权限'
    },
    {
      id: 3,
      title: '关于应用',
      desc: '版本、协议和反馈入口',
      badge: '信息'
    }
  ];

  private readonly items: SettingItem[] = [
    {
      id: 1,
      groupId: 1,
      title: '通知提醒',
      desc: '处理结果保存后,按提醒时间发送通知',
      type: 'switch',
      value: '',
      key: 'notify'
    },
    {
      id: 2,
      groupId: 1,
      title: '自动保存',
      desc: '识别结果确认后自动保存到本地记录',
      type: 'switch',
      value: '',
      key: 'autoSave'
    },
    {
      id: 3,
      groupId: 1,
      title: '深色模式',
      desc: '跟随系统外观,夜间查看内容时减少刺眼背景',
      type: 'switch',
      value: '',
      key: 'darkMode'
    },
    {
      id: 4,
      groupId: 1,
      title: '清理缓存',
      desc: '清理临时缩略图和识别过程缓存',
      type: 'action',
      value: '清理',
      key: 'cache'
    },
    {
      id: 5,
      groupId: 2,
      title: '相机权限',
      desc: '用于拍摄通知、票据和白板照片',
      type: 'link',
      value: '去开启',
      key: 'camera'
    },
    {
      id: 6,
      groupId: 2,
      title: '相册权限',
      desc: '用于从相册选择已有图片进行整理',
      type: 'link',
      value: '已授权',
      key: 'album'
    },
    {
      id: 7,
      groupId: 2,
      title: '麦克风权限',
      desc: '用于后续语音整理和会议内容识别',
      type: 'link',
      value: '去开启',
      key: 'microphone'
    },
    {
      id: 8,
      groupId: 2,
      title: '通知权限',
      desc: '用于发送待办提醒和处理结果提醒',
      type: 'link',
      value: '已授权',
      key: 'push'
    },
    {
      id: 9,
      groupId: 3,
      title: '当前版本',
      desc: '查看当前安装的应用版本',
      type: 'value',
      value: '1.0.0',
      key: 'version'
    },
    {
      id: 10,
      groupId: 3,
      title: '隐私政策',
      desc: '查看数据存储、权限使用和第三方服务说明',
      type: 'link',
      value: '查看',
      key: 'privacy'
    },
    {
      id: 11,
      groupId: 3,
      title: '用户协议',
      desc: '查看应用使用条款和免责声明',
      type: 'link',
      value: '查看',
      key: 'terms'
    },
    {
      id: 12,
      groupId: 3,
      title: '问题反馈',
      desc: '提交使用过程中遇到的问题或建议',
      type: 'link',
      value: '反馈',
      key: 'feedback'
    }
  ];

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

    return this.pageWidth;
  }

  private getPagePadding(): number {
    if (this.getEffectiveWidth() >= this.expandedThreshold) {
      return 24;
    }

    return 16;
  }

  // 分栏前先确认左侧分类栏、间距和右侧设置项区域都能放下
  private canUseSplitSettings(): boolean {
    const width = this.getEffectiveWidth();
    const availableWidth = width - this.getPagePadding() * 2;
    const requiredWidth = this.groupPanelWidth + this.twoColumnGap + this.detailMinWidth;

    return width >= this.expandedThreshold && availableWidth >= requiredWidth;
  }

  private isExpanded(): boolean {
    return this.canUseSplitSettings();
  }

  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 getSelectedGroup(): SettingGroup {
    const found = this.groups.find((item: SettingGroup) => item.id === this.selectedGroupId);
    return found ? found : this.groups[0];
  }

  private getItemsByGroup(groupId: number): SettingItem[] {
    return this.items.filter((item: SettingItem) => item.groupId === groupId);
  }

  private isSwitchOn(key: string): boolean {
    if (key === 'notify') {
      return this.notifyEnabled;
    }

    if (key === 'autoSave') {
      return this.autoSaveEnabled;
    }

    if (key === 'darkMode') {
      return this.darkModeEnabled;
    }

    return false;
  }

  private toggleSwitch(key: string) {
    if (key === 'notify') {
      this.notifyEnabled = !this.notifyEnabled;
    } else if (key === 'autoSave') {
      this.autoSaveEnabled = !this.autoSaveEnabled;
    } else if (key === 'darkMode') {
      this.darkModeEnabled = !this.darkModeEnabled;
    }
  }

  private handleAction(key: string) {
    if (key === 'cache') {
      this.cacheClearedCount += 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 HeaderPanel() {
    Column({ space: 10 }) {
      Row({ space: 10 }) {
        Column({ space: 4 }) {
          Text('设置页在 Pura X Max 上改成分组布局')
            .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 GroupBadge(text: string, selected: boolean) {
    Text(text)
      .fontSize(11)
      .fontColor(selected ? '#FFFFFF' : '#2F8F83')
      .padding({ left: 7, right: 7, top: 3, bottom: 3 })
      .backgroundColor(selected ? '#33FFFFFF' : '#E6F4F1')
      .borderRadius(999)
  }

  @Builder
  private GroupCard(item: SettingGroup) {
    Column({ space: 8 }) {
      Row() {
        Text(item.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.selectedGroupId === item.id ? '#FFFFFF' : '#111827')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Blank()

        this.GroupBadge(item.badge, this.selectedGroupId === item.id)
      }
      .width('100%')

      Text(item.desc)
        .fontSize(13)
        .fontColor(this.selectedGroupId === item.id ? '#DFF5F1' : '#6B7280')
        .lineHeight(19)
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .width('100%')
    .padding(14)
    .backgroundColor(this.selectedGroupId === item.id ? '#2F8F83' : '#FFFFFF')
    .borderRadius(20)
    .border({
      width: 1,
      color: this.selectedGroupId === item.id ? '#2F8F83' : '#E5E7EB'
    })
    .onClick(() => {
      this.selectedGroupId = item.id;
    })
  }

  @Builder
  private GroupPanel() {
    Column({ space: 14 }) {
      Column({ space: 4 }) {
        Text('设置分类')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#111827')

        Text('先选分类,再看对应设置项')
          .fontSize(13)
          .fontColor('#6B7280')
      }
      .width('100%')

      ForEach(this.groups, (item: SettingGroup) => {
        this.GroupCard(item)
      }, (item: SettingGroup) => item.id.toString())
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(26)
    .shadow({
      radius: 12,
      color: '#10000000',
      offsetX: 0,
      offsetY: 4
    })
  }

  @Builder
  private SwitchView(key: string) {
    Row() {
      if (this.isSwitchOn(key)) {
        Blank()

        Circle()
          .width(22)
          .height(22)
          .fill('#FFFFFF')
          .margin({ right: 3 })
      } else {
        Circle()
          .width(22)
          .height(22)
          .fill('#FFFFFF')
          .margin({ left: 3 })

        Blank()
      }
    }
    .width(48)
    .height(28)
    .backgroundColor(this.isSwitchOn(key) ? '#2F8F83' : '#CBD5E1')
    .borderRadius(14)
    .onClick(() => {
      this.toggleSwitch(key);
    })
  }

  @Builder
  private RightContent(item: SettingItem) {
    if (item.type === 'switch') {
      this.SwitchView(item.key)
    } else if (item.type === 'action') {
      Text(item.value)
        .fontSize(13)
        .fontColor('#2F8F83')
        .padding({ left: 10, right: 10, top: 6, bottom: 6 })
        .backgroundColor('#E6F4F1')
        .borderRadius(999)
        .onClick(() => {
          this.handleAction(item.key);
        })
    } else {
      Text(item.value)
        .fontSize(13)
        .fontColor(item.type === 'value' ? '#6B7280' : '#2F8F83')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
  }

  @Builder
  private SettingRow(item: SettingItem) {
    Row({ space: 12 }) {
      Column({ space: 4 }) {
        Text(item.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#111827')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

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

      this.RightContent(item)
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#FFFFFF')
    .borderRadius(20)
    .border({
      width: 1,
      color: '#E5E7EB'
    })
  }

  @Builder
  private SettingSection(group: SettingGroup) {
    Column({ space: 12 }) {
      Row() {
        Column({ space: 4 }) {
          Text(group.title)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#111827')

          Text(group.desc)
            .fontSize(13)
            .fontColor('#6B7280')
        }
        .layoutWeight(1)

        this.GroupBadge(group.badge, false)
      }
      .width('100%')
      .padding({ left: 4, right: 4 })

      ForEach(this.getItemsByGroup(group.id), (item: SettingItem) => {
        this.SettingRow(item)
      }, (item: SettingItem) => item.id.toString())
    }
    .width('100%')
  }

  @Builder
  private DetailPanel() {
    Column({ space: 14 }) {
      Row() {
        Column({ space: 4 }) {
          Text(this.getSelectedGroup().title)
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#111827')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          Text(this.getSelectedGroup().desc)
            .fontSize(13)
            .fontColor('#6B7280')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .layoutWeight(1)

        if (this.cacheClearedCount > 0) {
          Text('清理 ' + this.cacheClearedCount.toString() + ' 次')
            .fontSize(12)
            .fontColor('#6B7280')
        }
      }
      .width('100%')
      .padding({ left: 4, right: 4 })

      Scroll() {
        Column({ space: 12 }) {
          ForEach(this.getItemsByGroup(this.selectedGroupId), (item: SettingItem) => {
            this.SettingRow(item)
          }, (item: SettingItem) => item.id.toString())
        }
        .width('100%')
        .padding({ bottom: 24 })
      }
      .layoutWeight(1)
      .width('100%')
      .edgeEffect(EdgeEffect.Spring)
    }
    .width('100%')
    .height('100%')
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(26)
    .shadow({
      radius: 12,
      color: '#10000000',
      offsetX: 0,
      offsetY: 4
    })
  }

  @Builder
  private CompactSettingsList() {
    Scroll() {
      Column({ space: 22 }) {
        ForEach(this.groups, (group: SettingGroup) => {
          this.SettingSection(group)
        }, (group: SettingGroup) => group.id.toString())
      }
      .width('100%')
      .padding({ bottom: 24 })
    }
    .layoutWeight(1)
    .width('100%')
    .edgeEffect(EdgeEffect.Spring)
  }

  @Builder
  private MainContent() {
    if (this.isExpanded()) {
      Row({ space: this.twoColumnGap }) {
        Column() {
          this.GroupPanel()
        }
        .width(this.groupPanelWidth)
        .height('100%')
        .flexShrink(0)

        Column() {
          this.DetailPanel()
        }
        .layoutWeight(1)
        .height('100%')
      }
      .width('100%')
      .height('100%')
    } else {
      this.CompactSettingsList()
    }
  }

  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;
      }
    })
  }
}
相关推荐
浮芷.1 小时前
鸿蒙PC端 TTS 并发调用问题详解:资源竞争与队列管理
算法·华为·开源·harmonyos·鸿蒙·鸿蒙系统
小雨下雨的雨1 小时前
基于鸿蒙PC Electron框架技术完成的表单验证技术详解
前端·javascript·华为·electron·前端框架·鸿蒙
提子拌饭1331 小时前
饮料含糖量查询应用 - 鸿蒙PC用Electron框架完整实现
前端·javascript·华为·electron·前端框架·鸿蒙
nashane1 小时前
HarmonyOS 6学习:句柄泄漏(Fd Leak)从“崩溃现场”到“代码行”的精准狙击指南
学习·华为·音视频·harmonyos
坚果派·白晓明2 小时前
[鸿蒙PC三方库移植适配] 使用 AtomCode + Skills 自动完成Protobuf鸿蒙化适配
c语言·c++·华为·harmonyos
世人万千丶2 小时前
鸿蒙PC异常解决:Install Failed: error: failed to install bundle.
服务器·华为·开源·harmonyos·鸿蒙
小雨下雨的雨2 小时前
iOS风格计算器 - 鸿蒙PC Electron框架上的技术实现详解
游戏·ios·华为·electron·harmonyos·鸿蒙
小雨下雨的雨2 小时前
五子棋AI在鸿蒙PC Electron上的实现的原理与实践
人工智能·游戏·华为·electron·harmonyos·鸿蒙
AI_零食3 小时前
呼吸灯 - 通过鸿蒙PC Electron框架技术完成-在焦虑时代守护每一次呼吸的数字禅修
前端·javascript·华为·electron·前端框架·鸿蒙