一、首页整体结构

MeCharts 首页 HomeView 承载了所有量表入口,整体结构为:顶部导航栏 + 可滚动内容区(轮播图 + 分类网格)。
请添加图片描述

@Preview
@ComponentV2
export struct HomeView {
  @Local globalInfoModel: GlobalInfoModel = AppStorage.get('GlobalInfoModel') || new GlobalInfoModel()
  private pageContext: PageContext = AppStorage.get('pageContext') as PageContext;
  @Local imgHeight: number = 190
  @Local imgWidth: number = 264
  @Local imgList: Resource[] = new Array(3).fill('').map((item: string, index) => {
    return $r(`app.media.banner_0${index + 1}`)
  })
  private asdList: AbcModel[] = [
    new AbcModel($r('app.media.ic_item_abc'), 'ABC量表', '(自闭症行为评定量表)', 1),
    new AbcModel($r('app.media.ic_item_cars'), 'CARS量表', '(儿童期孤独症评定量表)', 2),
    new AbcModel($r('app.media.ic_item_move'), '多动症量表', '(多动症诊断标准量表)', 3),
  ]
  private careerList: AbcModel[] = [
    new AbcModel($r('app.media.ic_item_mbti'), 'MBTI测试', '(MBTI职业性格测试完整版)', 1),
  ]
  private emotionList: AbcModel[] = [
    new AbcModel($r('app.media.ic_item_depress'), 'SDS量表', '(抑郁自评量表)', 1),
    new AbcModel($r('app.media.ic_item_anxious'), 'SAS量表', '(焦虑症自评量表)', 2),
    new AbcModel($r('app.media.ic_item_relationship'), 'ECR量表', '(亲密关系经历量表)', 3),
  ]

  build() {
    Column() {
      this.buildTopBar();
      this.buildContentView();
    }.height('100%').width('100%').backgroundColor($r('app.color.color_background'))
  }
}

1.1 数据模型设计

所有量表入口使用统一的 AbcModel 数据模型:

@Observed
export class AbcModel {
  img: Resource | undefined = undefined
  title: string = ''
  subTitle: string = ''
  id: number = 0
  score?: string = ''
  type?: number = 0

  constructor(img: Resource, title?: string, subTitle?: string, id?: number, score?: string, type?: number) {
    // 构造函数赋值...
  }
}

@Observed 装饰器使该类的实例可被 ArkUI 框架观测,当属性变化时自动触发 UI 刷新。这在历史记录页面中更新分数显示时非常关键。

二、Swiper 轮播组件

2.1 基本使用

首页顶部使用 UISwiper(AGConnect 提供的增强轮播组件)展示推荐量表:

UISwiper({
  imgWidth: this.imgWidth,
  imgHeight: this.imgHeight,
  imgList: this.imgList,
  isLoop: true,
  isCovered: false,
  onImageClick: (index: number) => {
    if (index === 0) {
      this.pageContext.openPage({
        param: { title: 'MBTI测试' } as ResultParams,
        routerName: 'MbtiListPage',
      }, true);
    } else if (index === 1) {
      this.pageContext.openPage({
        param: { title: 'ABC量表' } as ResultParams,
        routerName: 'AbcListPage',
      }, true);
    } else if (index === 2) {
      this.pageContext.openPage({
        param: { title: 'SAS量表', type: FormType.TYPE_SDS } as ResultParams,
        routerName: 'MoveListPage',
      }, true);
    }
  }
})

轮播图的点击事件通过 onImageClick 回调处理,根据索引跳转到不同量表页面。

2.2 原生 Swiper 的使用方式

如果使用原生 Swiper 组件,基础用法如下:

Swiper() {
  ForEach(this.imgList, (img: Resource) => {
    Image(img)
      .width(this.imgWidth)
      .height(this.imgHeight)
      .borderRadius(12)
      .onClick(() => {
        // 点击跳转逻辑
      })
  })
}
.autoPlay(true)
.interval(3000)
.indicator(true)
.loop(true)
.cachedCount(2)

原生 Swiper 的关键属性:

属性 说明
autoPlay 是否自动播放
interval 自动播放间隔(ms)
indicator 是否显示导航指示器
loop 是否循环播放
cachedCount 预缓存页面数

三、Grid 网格布局

3.1 分类网格实现

首页使用 Grid 布局展示三大分类的量表入口:

@Builder
buildAbc() {
  // 分类标题
  Row({ space: 10 }) {
    Text().width(3).height(15).backgroundColor($r('app.color.color_default'));
    Text('自闭症相关').fontWeight(FontWeight.Bold).fontSize(16)
  }.margin({ left: 12 })

  // 量表网格
  Grid() {
    ForEach(this.asdList, (asd: AbcModel, index) => {
      GridItem() {
        Column({ space: 8 }) {
          Row({ space: 8 }) {
            Image(asd.img).width(24);
            Text(asd.title).fontWeight(FontWeight.Medium);
          };
          Text(asd.subTitle).fontSize(12);
        }
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .borderRadius(15)
        .width('100%')
        .height('100%')
        .backgroundColor($r('app.color.color_card'))
      }.onClick(() => {
        // 跳转到对应量表页面
      })
    })
  }
  .padding(12)
  .columnsTemplate('1fr 1fr')    // 两列等宽
  .rowsTemplate('1fr 1fr')       // 两行等高
  .columnsGap(10)
  .rowsGap(10)
  .width('100%')
  .height(160)
}

3.2 Grid 核心属性

columnsTemplate / rowsTemplate 是 Grid 最核心的属性,定义了网格的行列结构:

.columnsTemplate('1fr 1fr')   // 两列,每列等宽
.rowsTemplate('1fr 1fr')      // 两行,每行等高

fr 是分数单位,1fr 1fr 表示两列各占 1/2。更复杂的布局如:

.columnsTemplate('1fr 2fr 1fr')  // 三列,中间列是两侧的两倍宽
.rowsTemplate('auto auto')       // 两行,高度由内容决定

columnsGap / rowsGap 控制行列间距:

.columnsGap(10)   // 列间距 10vp
.rowsGap(10)      // 行间距 10vp

3.3 不同分类的布局差异

MeCharts 根据量表数量灵活调整布局:

分类 量表数量 布局方式 高度
自闭症相关 3个 2列×2行 160
职业性格相关 1个 2列×1行 80
情绪相关 3个 2列×2行 160

职业性格分类只有一个量表,使用单行布局;其他分类使用 2×2 网格,空位自然留白。

四、TopBar 通用顶部导航栏

4.1 组件设计

TopBar 是全应用复用率最高的组件,支持标题、返回按钮和右侧自定义内容:

@ComponentV2
export struct TopBar {
  private globalInfoModel: GlobalInfoModel = AppStorage.get('GlobalInfoModel') || new GlobalInfoModel();
  @Param title: string = ''
  @Param showBackButton: boolean = true
  @Param bgColor: ResourceColor = $r('app.color.color_default')
  @Param titleColor: ResourceColor = Color.White
  @Param barHeight: number = 80
  @Param statusBarHeight: number = this.globalInfoModel.statusBarHeight
  @Event onBack?: () => void | boolean
  @BuilderParam rightContent?: () => void

  handleBack() {
    if (this.onBack) {
      const result = this.onBack()
      if (result === false) {
        return    // 返回 false 可阻止默认行为
      }
    }
  }

  build() {
    RelativeContainer() {
      if (this.showBackButton) {
        Image($r('app.media.ic_back'))
          .width(20).height(20)
          .id('backBtn')
          .alignRules({
            left: { anchor: '__container__', align: HorizontalAlign.Start },
            center: { anchor: '__container__', align: VerticalAlign.Bottom }
          })
          .onClick(() => { this.handleBack() })
      }

      Text(this.title)
        .fontSize(18).fontWeight(FontWeight.Bold).fontColor(this.titleColor)
        .id('title')
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Bottom },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })

      if (this.rightContent) {
        Row() { this.rightContent() }
          .id('rightContent')
          .alignRules({
            right: { anchor: '__container__', align: HorizontalAlign.End },
            center: { anchor: '__container__', align: VerticalAlign.Bottom }
          })
      }
    }
    .width('100%')
    .height(this.barHeight)
    .backgroundColor(this.bgColor)
    .padding({
      top: this.statusBarHeight,
      bottom: 20,
      left: 12,
      right: 12
    })
  }
}

4.2 RelativeContainer 相对布局

TopBar 使用 RelativeContainer 实现左右中三栏布局,这是 HarmonyOS 提供的相对定位容器:

  • 返回按钮:锚定容器左侧 + 垂直居中
  • 标题:锚定容器水平居中 + 垂直居中
  • 右侧内容:锚定容器右侧 + 垂直居中

相比 Flex 布局,RelativeContainer 在处理"左右对齐 + 中间居中"的场景时更简洁。

4.3 使用示例

首页使用 TopBar(无返回按钮 + 自定义右侧):

TopBar({
  title: '自测量表',
  showBackButton: false,
  rightContent: () => {
    this.buildRightContent()
  },
})

子页面使用 TopBar(有返回按钮):

TopBar({
  title: this.title,
  onBack: () => {
    this.pageContext.popPage(true)
  }
})

五、@ComponentV2 与 @Component 的选择

MeCharts 中两种组件声明方式并存:

  • @ComponentV2:新版本装饰器,支持 @Param@Event@Local@Monitor 等,类型安全更好
  • @Component:经典版本装饰器,支持 @Prop@Link@State
特性 @Component @ComponentV2
状态装饰器 @State, @Prop, @Link @Local, @Param, @Event
状态观测 基于代理 基于拦截器,性能更优
参数传递 构造函数传入 @Param 声明式
事件回调 箭头函数 @Event 装饰器
监听变化 @Watch @Monitor

新项目推荐优先使用 @ComponentV2

六、小结

本篇讲解了首页的 Swiper 轮播、Grid 网格布局和 TopBar 通用组件的设计实现。通过组件化封装和统一数据模型,首页的代码结构清晰、复用性强。下一篇将深入量表答题页面的实现,讲解自定义 Radio 组件与表单交互。

Logo

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

更多推荐