HarmonyOS《柚兔学伴》项目实战25-我的页面、Web 嵌入与项目总结

25. 我的页面、Web 嵌入与项目总结

本章导读

「我的」页面是应用的个人中心入口,承载用户信息、积分管理、系统设置等功能。本章将详解 MineView 的登录状态管理、WebPage 的网页嵌入、SettingPage 与 AboutPage 的设置体系,并对整个项目的架构设计与最佳实践进行全面总结。


25.1 MineView 个人中心

用户信息展示

MineView 展示头像、昵称和登录状态,未登录时点击头像触发登录:

typescript 复制代码
// MineView.ets
@Component
export struct MineView {
  @State isLoggedIn: boolean = false;
  @State userInfo: string = '未登录';
  @State userNickname: string = '柚兔';
  @State avatar: Resource | string = $r('app.media.ic_avatar');

  aboutToAppear(): void {
    this.updateLoginState()
  }

  build() {
    Column() {
      Row({ space: 10 }) {
        Image(this.avatar).width(48)
          .clip(true)
          .borderRadius(30)
          .onClick(() => {
            if (!this.isLoggedIn) {
              this.login()
            }
          })
        Column({ space: 8 }) {
          Text(this.userInfo).fontSize(16).fontWeight(FontWeight.Bold)
          Text('书山有路勤为径').fontColor($r('app.color.color_sub_text')).fontSize(12)
        }.layoutWeight(1).alignItems(HorizontalAlign.Start)

        Image($r('app.media.ic_setting')).width(32)
          .onClick(() => {
            this.pageContext.openPage({
              routerName: 'SettingPage',
              onReturn: async (data) => {
                this.updateLoginState()
              }
            }, true);
          })
      }
      .backgroundColor($r('app.color.color_card'))
      .borderRadius(5).padding(12)
    }
  }
}

登录流程

登录通过 BackupManager 华为账号服务实现:

typescript 复制代码
login() {
  const backupManager = BackupManager.getInstance();
  if (!BackupManagerService.getInstance().isBackupManagerInitialized()) {
    promptAction.showToast({ message: '等待初始化完成' });
    return;
  }
  this.isLoggedIn = UserInfoManager.isLoggedIn();
  if (this.isLoggedIn) return;

  try {
    backupManager.login();
    setTimeout(() => {
      this.updateLoginState();
    }, 1000);
  } catch (error) {
    console.error('登录失败:', error);
  }
}

登录状态更新

typescript 复制代码
private updateLoginState() {
  this.isLoggedIn = UserInfoManager.isLoggedIn();
  if (this.isLoggedIn) {
    const userInfoData = UserInfoManager.getUserInfo();
    this.userInfo = `${userInfoData?.nickName || '未登录'}`;
    this.avatar = userInfoData?.avatarUri || $r('app.media.ic_avatar')
  } else {
    this.userInfo = '未登录';
    this.avatar = $r('app.media.ic_avatar')
  }
}

UserInfoManager 封装了华为账号的用户信息读取,isLoggedIn() 判断登录状态,getUserInfo() 获取昵称和头像 URI。

CardItem 列表导航

MineView 使用通用 CardItem 组件构建功能列表:

typescript 复制代码
Column({ space: 12 }) {
  CardItem({ name: '我的积分' }).onClick(() => {
    this.pageContext.openPage({ routerName: 'ScorePage' }, true);
  })
  CardItem({ name: '反馈建议' }).onClick(() => {
    this.pageContext.openPage({
      param: {
        title: '反馈建议',
        url: 'https://ncn1rpfvd5vb.feishu.cn/share/base/form/shrcnwljf1ptBZVyKfmWH3eS6Ih',
      } as ResultParams,
      routerName: 'WebPage',
    }, true);
  })
}

通过 ResultParams 传递标题和 URL 给 WebPage,实现从原生到 Web 的无缝跳转。


25.2 WebPage 网页嵌入

核心实现

WebPage 使用 ArkUI 的 Web 组件嵌入网页,配合 webview.WebviewController 控制页面行为:

typescript 复制代码
// WebPage.ets
import { webview } from '@kit.ArkWeb';

@Component
struct WebPage {
  controller: webview.WebviewController = new webview.WebviewController();
  @State url: string = ''
  @State title: string = ''
  @State isLoading: boolean = true

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

        Stack() {
          Web({ src: this.url, controller: this.controller })
            .layoutWeight(1)
            .onPageEnd(() => {
              this.isLoading = false
            })
          LoadingProgress().width(40).height(40)
            .visibility(this.isLoading ? Visibility.Visible : Visibility.None)
        }.layoutWeight(1)
      }
    }
    .onReady((ctx: NavDestinationContext) => {
      const params = ctx.pathInfo.param as ResultParams;
      this.url = params.url as string
      this.title = params.title as string
    })
  }
}

ResultParams 接口

typescript 复制代码
export interface ResultParams {
  score?: number
  firstScore?: number
  secondScore?: number
  thirdScore?: number
  forthScore?: number
  oddScore?: number
  evenScore?: number
  type?: number
  title?: string
  url?: string
}

该接口同时服务多个页面(积分详情、Web 页面等),通过 titleurl 字段驱动 WebPage 的标题栏和网页地址。

加载状态处理

onPageEnd 回调在网页加载完成时触发,关闭 LoadingProgress 指示器:

typescript 复制代码
.onPageEnd(() => {
  this.isLoading = false
})

isLoading 默认为 true,页面初始即显示加载动画,加载完成后切换为 Web 内容。


25.3 SettingPage 设置页

SettingPage 提供隐私政策入口与退出登录功能:

typescript 复制代码
// SettingPage.ets
@Component
struct SettingPage {
  build() {
    NavDestination() {
      Column() {
        TopBar({ title: '设置', onBack: () => { this.pageContext.popPage(true) } })

        Column({ space: 12 }) {
          CardItem({ name: '儿童隐私政策' }).onClick(() => {
            this.pageContext.openPage({
              param: {
                title: '儿童隐私政策',
                url: 'http://www.ai-youtoo.cn/partner_children_privacy.html',
              } as ResultParams,
              routerName: 'WebPage',
            }, true);
          })
          CardItem({ name: '关于我们' }).onClick(() => {
            this.pageContext.openPage({ routerName: 'AboutPage' }, true);
          })
        }
        .backgroundColor($r('app.color.color_card'))
        .borderRadius(12).padding(12)

        Blank().layoutWeight(1)

        Button('退出登录', { type: ButtonType.Normal })
          .backgroundColor($r('app.color.app_alert'))
          .onClick(async () => {
            UserInfoManager.clearUserInfo()
            this.pageContext.popPage(true, new Object({ index: 0 }))
          });
      }
    }
  }
}

退出登录时调用 UserInfoManager.clearUserInfo() 清除本地用户数据,然后返回上一页。MineView 通过 onReturn 回调自动刷新登录状态。


25.4 AboutPage 关于页

AboutPage 展示应用品牌信息:

typescript 复制代码
// AboutPage.ets
@Component
struct AboutPage {
  build() {
    NavDestination() {
      Column({ space: 10 }) {
        TopBar({ title: '关于我们', onBack: () => { this.pageContext.popPage(true) } })

        Column({ space: 10 }) {
          Image($r('app.media.startIcon')).width(60).height(60).margin({ top: 20 })

          Text('柚兔学伴')
            .fontSize(18)
            .margin({ top: 10, bottom: 40 })

          Blank().layoutWeight(1)

          Text('柚兔(山东)网络科技有限责任公司')
            .fontSize(12).fontColor($r('app.color.color_text')).margin({ top: 30 })
          Text('鲁ICP备2024092126号-4A')
            .fontSize(12).fontColor($r('app.color.color_text'))
        }
      }
    }
  }
}

布局采用 Blank().layoutWeight(1) 将公司信息推至底部,营造居中对称的视觉感。


25.5 项目架构总览

九模块架构

复制代码
StudyPartner/
├── entry/              # 主入口模块(EntryAbility、HomePage)
├── common/             # 公共模块(工具类、管理器、通用组件)
├── feature/
│   ├── todo/           # 待办与诗词模块
│   ├── stroke/         # 笔画查询模块
│   └── my/             # 个人中心模块
├── backup_air/         # 云备份与登录模块
├── network/            # 网络请求模块
└── smartxplayer/       # 音频播放模块

模块化优势:

  • 功能边界清晰,团队可并行开发
  • 模块可独立编译测试,提升构建效率
  • 公共能力统一收敛到 common,避免代码重复

MVVM 模式

复制代码
View (Component)  ←→  ViewModel (Model类)  ←→  Model (Database/Preferences)

以 StrokeView 为例:

  • View:StrokeView 组件,负责 UI 渲染与用户交互
  • ViewModel:StrokeModel 单例,管理笔画数据与 AI 对话状态
  • Model:CharacterService + 云端 API,提供汉字数据

25.6 最佳实践总结

通用组件抽取

项目抽取了高频使用的通用组件:

组件 用途 使用位置
TopBar 顶部导航栏 所有二级页面
CardItem 列表行项 MineView、SettingPage
IconText 图标+文字功能入口 TodoView
LoadingView 全局加载态 TodoView
CustomImageToggle 自定义开关 TodoView 任务完成

抽取原则: 同一 UI 模式出现 3 次以上即抽取为通用组件。

数据库封装模式

所有 RDB 数据库遵循统一封装范式:

typescript 复制代码
class XxxDatabase {
  private store: relationalStore.RdbStore | null = null;

  async initialize() { /* getRdbStore + executeSql */ }
  async addXxx()      { /* insert */ }
  async getXxxById()  { /* query + equalTo */ }
  async getAllXxx()   { /* query + orderByDesc */ }
  async updateXxx()   { /* update + equalTo */ }
  async deleteXxx()   { /* delete + equalTo */ }
  async close()       { /* store.close */ }

  private resultSetToXxx() { /* ResultSet → 接口对象 */ }
}

TodoDatabase、GiftExchangeDatabase 均遵循此模式,新增数据实体只需替换表名和字段。

单例模式

需要全局共享状态的管理器使用单例:

typescript 复制代码
BackupManager.getInstance()
UserInfoManager (静态方法)
RecordUtils.getInstance()
BreakpointSystem
HttpManager

控制器模式

组件间通信采用控制器解耦:

  • TimerComponentController:外部控制倒计时启动/暂停/重置
  • LottieController:控制 Lottie 动画播放/暂停/停止
  • webview.WebviewController:控制 Web 页面前进/后退/刷新

全局状态管理

typescript 复制代码
// 写入
AppStorage.setOrCreate('GlobalInfoModel', model)

// 只读绑定
@StorageProp('GlobalInfoModel') globalInfoModel: GlobalInfoModel = AppStorage.get('GlobalInfoModel')!

@StorageProp 单向绑定,UI 只读不写;@StorageLink 双向绑定,UI 可修改。


25.7 性能优化要点

LazyForEach 懒加载

长列表使用 LazyForEach 替代 ForEach,按需加载列表项:

typescript 复制代码
List() {
  LazyForEach(this.dataSource, (item) => {
    ListItem() { /* ... */ }
  })
}

ForEach 一次性创建所有节点,LazyForEach 仅渲染可视区域,大幅减少内存占用。

图片缓存与资源管理

  • 图片资源使用 $r() 引用,系统自动管理缓存
  • Lottie 动画在 aboutToDisappear 中清理控制器
  • Canvas 上下文在页面销毁时置空

定时器清理

所有 setInterval 必须在 aboutToDisappear 中清除:

typescript 复制代码
aboutToDisappear(): void {
  if (this.internalId) {
    clearInterval(this.internalId)
  }
  if (this.strokeManager != null) {
    this.strokeManager.stopAnimation();
    this.strokeManager.resetAnimation()
  }
  if (this.context) {
    this.context.clearRect(0, 0, 100, 100);
    this.context = null
  }
}

25.8 发布准备

签名配置

Release 版本需要配置正式签名:

json5 复制代码
// build-profile.json5
{
  "signingConfigs": [{
    "name": "release",
    "type": "HarmonyOS",
    "material": {
      "certpath": "certs/release.cer",
      "storePassword": "...",
      "keyAlias": "release",
      "keyPassword": "...",
      "profile": "certs/release.p7b",
      "signAlg": "SHA256withECDSA",
      "storeFile": "certs/release.p12"
    }
  }]
}

版本管理

  • versionCode:递增整数,每次发布 +1
  • versionName:语义化版本号(如 1.0.0)

严格模式合规

ArkTS 严格模式下需注意:

  • 禁止使用 any 类型
  • 禁止运行时类型变更
  • 必须显式初始化类属性
  • 空安全:可选属性需用 ?. 或非空断言 !!

全系列总结

至此,「柚兔学伴」HarmonyOS 项目教程全 25 讲完结。我们从项目创建与基础导航开始,逐步覆盖了:

  • 基础架构:多模块工程、Navigation 导航、TabBar 布局
  • 数据持久化:Preferences 轻量存储、RDB 关系数据库
  • 核心功能:待办管理、诗词朗读、笔画查询、字帖生成、PDF 导出
  • 进阶能力:AI 智能体对话、Lottie 动画、Canvas 绘制、语音交互
  • 生态集成:华为账号登录、云存储、Web 嵌入、系统文件选择器
  • 工程实践:组件抽取、单例模式、控制器解耦、性能优化、发布流程

HarmonyOS 的 ArkUI 声明式开发范式为儿童教育应用提供了丰富而高效的能力支撑。希望本系列能帮助开发者在鸿蒙生态中快速构建高质量应用。

相关推荐
小林ixn3 小时前
从零到一理解 React 父子组件通信:手写一个 Todo 应用带你彻底搞懂单向数据流
前端·javascript·react.js
三声三视3 小时前
uni-app 鸿蒙端传参变成 [object Object]?顺着源码追到 ArkTS router 底层才搞明白
人工智能·ai·uni-app·aigc·ai编程·harmonyos
达子6664 小时前
第7章_HarmonyOS 图解 Ability公共事件与通知
华为·harmonyos
爱写代码的阿森4 小时前
鸿蒙三方库 | harmony-utils之KvUtil键值型数据库操作详解
数据库·华为·harmonyos·鸿蒙·huawei
醇氧4 小时前
CountDownLatch / CyclicBarrier / Semaphore 面试高频问答清单
前端·面试·职场和发展
爱写代码的阿森6 小时前
鸿蒙三方库 | harmony-utils之PreferencesUtil用户首选项读写详解
华为·harmonyos·鸿蒙·huawei
qetfw6 小时前
MXU:Tauri 2 + React 的 MaaFramework 跨平台 GUI 源码
前端·python·react.js·前端框架·开源项目·效率工具
甲维斯6 小时前
我要开始吹牛逼了!Kimi K3 “宇宙无敌”!
前端·人工智能