当前位置: 首页 > news >正文

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

前言

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

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

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

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

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

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

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

一、设置页不能只拉宽

1.1 外屏单列适合连续浏览

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

最常见的写法是这样的。

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

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

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

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

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

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

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

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

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

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

2.1 分类要从标题变成入口

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

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

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

@State private selectedGroupId: number = 1;

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

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

2.2 设置项要用同一套数据

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

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

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

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

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

三、分栏前先保住右侧

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

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

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

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

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

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

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

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。

附:完整代码

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; } }) } }
http://www.cnnetsun.cn/news/2728392.html

相关文章:

  • 【AI测试革命白皮书】:2024年全球头部科技公司已落地的7大智能测试整合范式
  • ArcMap布局视图实战:一张图搞定站点分布主图+全国位置副图(含比例尺指北针)
  • 3步掌握跨平台数据迁移:开源宝可梦存档编辑器完全指南
  • 利用个人设备构建分布式麦克风阵列实现高精度会议转录
  • 终极开源IPAM解决方案:NIPAP如何让IP地址管理变得简单高效
  • 告别高光干扰!用Python+OpenCV复现并行单像素成像,搞定复杂光照下的3D重建
  • DIY动圈式纸板扬声器:从电磁原理到动手制作的完整指南
  • QKeyMapper技术架构深度解析:跨设备输入映射与虚拟化方案实现
  • 从结绳记事到5G基站:用大唐杯仿真游戏串讲通信技术发展史(附避坑指南)
  • 界面自动化测试范式重构:Pywinauto Recorder在Windows生态中的战略定位与技术突破
  • 基于树莓派与热敏打印机的DIY拍立得相机:从硬件集成到软件控制全流程解析
  • C#工业通信开发包:EtherNet/IP协议栈源码,含IO适配器示例与PC测试工具
  • Office Tab Enterprise 12.00直装版:为Word/Excel/PPT/Outlook加标签,免注册适配2016与365
  • PyCharm玩家专属:用虚拟环境从源码跑通X-Anylabeling图像标注工具(含清华镜像加速)
  • DIY 12V 18Ah磷酸铁锂电池组:从电芯筛选到BMS安装全流程解析
  • 基于Makey Makey与Scratch的简易猫驱赶器制作指南
  • 用Espruino和JavaScript打造电动滑板遥控器:从硬件选型到固件开发全解析
  • RHEL8系统管理员必看:用yum和ELRepo源安全升级内核到最新稳定版(附kernel-ml与kernel-lt选择指南)
  • 运维效率翻倍:Xmanager + Xstart一键脚本,快速部署与管理多台Linux服务器桌面
  • 基于Arduino与火焰传感器的智能火灾报警系统设计与实现
  • SOAP 消息级认证在 SAP Web Service 集成里的落地逻辑
  • 微软对话语音识别达人类水平:技术拆解与工程实践
  • Hotkey Detective:3分钟精准定位Windows热键冲突的智能侦探
  • 终极老旧Mac升级指南:3步突破苹果限制,让旧设备焕发新生
  • 告别‘yum不可用’:银河麒麟V10系统盘挂载与软件源配置的三种高效玩法
  • Beyond Compare 5密钥生成器:告别30天限制的三种高效方案
  • Emotion_text_classifier性能优化指南:NPU加速与推理效率提升
  • PVE-VDIClient:5分钟搭建企业级虚拟桌面基础设施的完整指南
  • Excel LAMBDA函数终极指南:从自定义函数到递归与动态数组实战
  • 终极网盘下载助手:免费开源工具帮你突破9大网盘下载限制