HarmonyOS 实战教程(八):个人中心与华为云服务集成 —— 以「柚兔自测量表」为例

一、个人中心页面概览

个人中心(MineView)提供用户信息展示、登录/登出、功能入口(历史记录、反馈建议、关于我们)和版本信息展示。

typescript 复制代码
@Component
export struct MineView {
  @State private isLoggedIn: boolean = false;
  @State private userName: string = '点我登录';
  @State private avatar: Resource | string = $r('app.media.ic_head');
  @State versionName: string = 'V1.0.1';
  private pageContext: PageContext = AppStorage.get('pageContext') as PageContext;

  async aboutToAppear(): Promise<void> {
    this.updateLoginState()
    this.versionName = await getAppVersion()
  }

  build() {
    Column() {
      this.buildTopBar();
      Column() {
        this.buildUserInfo();
        this.buildSecondRow();
        Blank().layoutWeight(1)
        this.buildVersionInfo();
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.color_background'))
  }
}

二、华为一键登录集成

2.1 BackupManagerService 初始化

MeCharts 使用 backup_air 三方库集成华为云服务,包括一键登录和数据备份:

typescript 复制代码
export default class BackupManagerService {
  private static instance: BackupManagerService;
  private isInitialized: boolean = false;
  private isLoggedIn: boolean = false;

  public static getInstance(): BackupManagerService {
    if (!BackupManagerService.instance) {
      BackupManagerService.instance = new BackupManagerService();
    }
    return BackupManagerService.instance;
  }

  public async initializeBackupManager(): Promise<void> {
    try {
      const backupManager = BackupManager.getInstance();
      let store = (await DbUtil.getInstance(getContext().getApplicationContext())).store;

      if (!store) {
        throw new Error('未初始化数据库');
      }

      backupManager.init({
        onCompleteBackup: () => {
          console.log('自定义回调- 备份完成');
        },
        onCompleteRestore: () => {
          console.log('自定义回调- 恢复完成');
        },
        updateDbVersion: (cloudDbVersion) => {
          console.log("自定义回调- 更新数据库版本 云端数据库版本" + cloudDbVersion);
        },
        cloudDir: `testUser`,
        cloudDbName: 'Partner.db',
        storeConfig: DbUtil.STORE_CONFIG,
        backupDirs: [new BackupDir("testDir", "testCloud.zip")],
        cloudStorageType: 'sdk',
        bucketName: "partner-xhmds",
        productId: '461323198430551180',
        client_id: '1788670196915914048',
        client_secret: '***',
        oauth_client_id: "6917585955626292994",
        oauth_client_secret: '***',
      }, store);

      this.isInitialized = true;
      this.isLoggedIn = true;
    } catch (error) {
      this.isInitialized = false;
      this.isLoggedIn = false;
    }
  }
}

初始化参数说明:

参数 说明
onCompleteBackup 备份完成回调
onCompleteRestore 恢复完成回调
updateDbVersion 云端数据库版本变更回调
cloudDir 云存储用户目录
cloudDbName 云端数据库名称
cloudStorageType sdk 需登录,http 不需登录
bucketName 存储桶名称
productId AGC 项目 ID

2.2 登录流程

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();
      if (this.isLoggedIn) {
        // 登录成功
      } else {
        promptAction.showToast({ message: '登录失败' });
      }
    }, 1000);
  } catch (error) {
    promptAction.showToast({ message: '登录失败' });
  }
}

backupManager.login() 调用华为一键登录服务,无需用户输入账号密码即可完成认证。

2.3 登录状态管理

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_head')
  } else {
    this.userInfo = '未登录';
    this.avatar = $r('app.media.ic_head')
  }
}

2.4 登出功能

typescript 复制代码
@Builder
buildRightContent() {
  Image($r('app.media.ic_logout'))
    .width(24).height(24)
    .onClick(() => {
      UserInfoManager.clearUserInfo()
      this.updateLoginState()
    })
    .visibility(this.isLoggedIn ? Visibility.Visible : Visibility.None)
}

登出按钮只在已登录状态显示,点击后清除用户信息并刷新 UI。

三、用户信息展示

3.1 用户头像与昵称

typescript 复制代码
@Builder
buildUserInfo() {
  Column() {
    Image(this.avatar)
      .width(60).height(60)
      .borderRadius(40)

    Row() {
      Text(this.userInfo)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isLoggedIn ? '#333333' : '#999999')
    }
    .margin({ top: 10 })
  }
  .width('100%')
  .padding(20)
  .margin({ top: 30 })
  .onClick(() => {
    if (!this.isLoggedIn) {
      this.login()
    }
  })
}

未登录时显示默认头像和灰色文字"未登录",点击触发登录;已登录时显示用户头像和昵称。

四、功能卡片入口

4.1 通用卡片组件

typescript 复制代码
@Builder
buildCard(icon: Resource, title: string, onClick: () => void) {
  Column() {
    Image(icon).width(40).height(40)
    Text(title).fontSize(14).margin({ top: 8 })
  }
  .width(100).height(100)
  .justifyContent(FlexAlign.Center)
  .backgroundColor($r('app.color.color_card'))
  .borderRadius(12)
  .shadow({
    radius: 8,
    color: '#1a000000',
    offsetX: 0,
    offsetY: 2
  })
  .onClick(onClick)
}

4.2 功能入口布局

typescript 复制代码
@Builder
buildSecondRow() {
  Row() {
    this.buildCard($r("app.media.ic_record"), '历史记录', () => {
      this.pageContext.openPage({
        routerName: 'RecordPage',
      }, true);
    });

    this.buildCard($r('app.media.ic_feedback'), '反馈建议', () => {
      this.pageContext.openPage({
        param: {
          title: '反馈建议',
          url: 'https://ncn1rpfvd5vb.feishu.cn/share/base/form/shrcn0YEc3umGFsTAcZimonouTC',
        } as ResultParams,
        routerName: 'WebPage',
      }, true);
    });

    this.buildCard($r('app.media.ic_praise'), '关于我们', () => {
      this.pageContext.openPage({
        routerName: 'AboutPage',
      }, true);
    });
  }
  .width('100%')
  .justifyContent(FlexAlign.SpaceEvenly)
  .margin({ top: 20 })
  .padding({ left: 20, right: 20 })
}

三个功能入口采用 SpaceEvenly 均匀排列,视觉上简洁对称。

五、版本信息获取

5.1 动态获取应用版本

typescript 复制代码
async function getAppVersion(): Promise<string> {
  try {
    const bundleInfo = await bundleManager.getBundleInfoForSelf(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
    );
    return bundleInfo.versionName
  } catch (err) {
    console.error('getBundleInfoForSelf failed:', JSON.stringify(err));
    return '1.0.1'
  }
}

bundleManager.getBundleInfoForSelf() 可获取当前应用的包信息,包括版本名、版本号等。无需声明额外权限。

5.2 版本信息展示

typescript 复制代码
@Builder
buildVersionInfo() {
  Text(`当前版本 ${this.versionName}`)
    .fontSize(12)
    .fontColor('#999999')
    .width('100%')
    .textAlign(TextAlign.Center)
    .margin({ top: 60, bottom: 20 })
}

六、关于页面

6.1 AboutPage 实现

typescript 复制代码
@Component
struct AboutPage {
  private pageContext: PageContext = AppStorage.get('pageContext') as PageContext;

  build() {
    NavDestination() {
      Column({ space: 10 }) {
        this.buildTopBar()

        Column({ space: 10 }) {
          Image($r('app.media.app_icon')).width(60).height(60).margin({ top: 20 })
          Text('柚兔自测量表').fontSize(18).margin({ top: 10, bottom: 40 })

          SettingItem({ name: '用户协议' }).onClick(() => {
            this.pageContext.openPage({
              param: { title: '用户协议', url: UrlConstants.URL_USER } as ResultParams,
              routerName: 'WebPage',
            }, true);
          }).width('100%')

          SettingItem({ name: '隐私政策' }).onClick(() => {
            this.pageContext.openPage({
              param: { title: '隐私政策', url: UrlConstants.URL_PRIVACY } as ResultParams,
              routerName: 'WebPage',
            }, true);
          }).width('100%')

          this.buildRow('作者微信', 'gy09312')

          Blank().layoutWeight(1)

          Text('如果您想添加某些功能或者有什么意见建议都可以通过以上途径联系到我,感谢您的反馈!')
            .fontSize(12).fontColor($r('app.color.color_text'))
            .padding(15)
          Text('鲁ICP备2024092126号-6A')
            .fontSize(12).fontColor($r('app.color.color_text'))
            .padding(15)
        }
        .width('100%')
        .layoutWeight(1)
      }
    }
  }
}

6.2 SettingItem 通用设置项组件

typescript 复制代码
@Component
export struct SettingItem {
  @Prop name: string
  @Prop content?: string

  build() {
    Row({ space: 7 }) {
      Text(this.name).fontWeight(FontWeight.Medium)
      Blank().layoutWeight(1)
      if (this.content) {
        Text(this.content).fontSize(12)
      }
      Image($r('app.media.ic_enter')).width(14)
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
    .padding({ left: 12, right: 12 })
  }
}

SettingItem 是一个简洁的设置项组件:左侧名称、右侧可选内容+箭头,支持 @Prop 数据传递。

七、Web 页面容器

用户协议、隐私政策和反馈建议都通过 WebPage 加载网页内容:

typescript 复制代码
@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
    })
  }
}

关键点:

  • webview.WebviewController 控制 Web 组件
  • onPageEnd 回调在页面加载完成后触发,关闭 loading
  • Stack 叠加 Web 和 LoadingProgress,实现加载指示器效果

八、emitter 事件驱动的跨页面通信

AI 咨询页面检测到未登录时,通过 emitter 通知主页切换到"我的"Tab:

typescript 复制代码
// ConsultView 中发送事件
if (!UserInfoManager.isLoggedIn()) {
  ToastUtil.showToast('请先登录...')
  let eventData: emitter.EventData = {
    data: { "content": "content" }
  };
  emitter.emit(CommonConstants.EVENT_ID, eventData);
}

// Index 中监听事件
aboutToAppear(): void {
  let callback: Callback<emitter.EventData> = (eventData: emitter.EventData) => {
    this.tabController.changeIndex(2)  // 切换到"我的"Tab
  };
  emitter.on(CommonConstants.EVENT_ID, callback);
}

aboutToDisappear(): void {
  emitter.off(CommonConstants.EVENT_ID);  // 移除监听
}

emitter 是 HarmonyOS 提供的进程内事件通知机制,适合跨组件/跨页面的松耦合通信。

九、小结

本篇讲解了个人中心的完整实现,包括华为一键登录集成、用户状态管理、功能入口布局和 Web 容器页面。通过 backup_air 三方库,MeCharts 轻松集成了华为云服务的登录与备份能力。下一篇将深入响应式布局与断点系统。

相关推荐
红烧大青虫2 小时前
HarmonyOS应用《玄象》开发实战:掷钱动画:animateTo + 缓动曲线的物理感模拟
harmonyos·鸿蒙
二流小码农3 小时前
鸿蒙开发:实现文本渐变效果
android·ios·harmonyos
程序员黑豆4 小时前
鸿蒙应用开发:Refresh + List 下拉刷新组件使用教程
前端·华为·harmonyos
fengxinzi_zack5 小时前
HarmonyOS应用《玄象》开发实战:MansionListPage 列表页:List / ListItem / LazyForEach 性能优化
harmonyos·鸿蒙
youtootech5 小时前
HarmonyOS 实战教程(九):响应式布局与断点系统 —— 以「柚兔自测量表」为例
华为·harmonyos
山璞6 小时前
将一个 Flutter 项目转为用 ArkUI-X 框架实现(4)
flutter·harmonyos
2501_919749036 小时前
华为鸿蒙隐私文件加密APP—小羊加密室
华为·harmonyos
程序员黑豆6 小时前
鸿蒙开发实战:使用 List 组件构建新闻列表
前端·华为·harmonyos
爱写代码的阿森7 小时前
鸿蒙三方库 | harmony-utils之RegexUtil正则匹配验证详解
服务器·华为·harmonyos·鸿蒙·huawei