HarmonyOS智慧农业管理应用开发教程--高高种地--第28篇:用户中心与个人资料

第28篇:用户中心与个人资料

📚 本篇导读

用户中心是应用的重要功能模块,为用户提供个人信息管理、账号设置、数据统计等功能。本篇教程将实现一个完整的用户中心系统,包括个人资料编辑、头像管理、模式切换等核心功能。

本篇将实现

  • 👤 个人资料管理(昵称、头像、联系方式、个人简介)
  • 📸 头像上传功能(拍照、相册选择、图片处理)
  • ⚙️ 账号设置功能(模式切换、通知设置、隐私安全)
  • 📊 数据管理(缓存清理、数据分析)
  • ℹ️ 关于与帮助(使用帮助、关于我们)

🎯 学习目标

完成本篇教程后,你将掌握:

  1. 如何设计用户中心页面布局
  2. 如何实现个人资料编辑功能
  3. 如何使用相机和相册选择图片
  4. 如何处理和存储图片
  5. 如何实现模式切换功能
  6. 用户数据的本地存储和管理

一、功能架构设计

1.1 用户中心功能结构

复制代码
用户中心(ServicesPage)
├── 个人信息区
│   ├── 用户头像(可点击更换)
│   ├── 用户昵称
│   ├── 用户身份(家庭园艺/专业农业)
│   └── 编辑按钮
│
├── 账户设置区
│   ├── 个人信息(跳转到编辑页面)
│   ├── 切换模式(弹窗选择)
│   ├── 消息通知
│   └── 隐私安全
│
├── 应用设置区
│   ├── 数据管理
│   └── 数据分析
│
└── 关于区
    ├── 使用帮助
    └── 关于我们

1.2 页面关系图

复制代码
ServicesPage(服务与设置)
    ├─→ ProfileEditPage(编辑资料)
    ├─→ NotificationSettingsPage(消息通知)
    ├─→ PrivacySettingsPage(隐私安全)
    ├─→ DataManagementPage(数据管理)
    ├─→ DataAnalysisPage(数据分析)
    ├─→ HelpPage(使用帮助)
    └─→ AboutPage(关于我们)

二、实现服务与设置页面

2.1 页面结构分析

ServicesPage 是用户中心的主页面,包含以下几个部分:

  1. 顶部导航栏:显示"服务与设置"标题
  2. 用户信息卡片:展示头像、昵称、身份,提供编辑入口
  3. 账户设置区:个人信息、模式切换、通知、隐私等
  4. 应用设置区:数据管理、数据分析
  5. 关于区:帮助、关于我们

2.2 完整代码实现

文件位置entry/src/main/ets/pages/Services/ServicesPage.ets

这个文件已经存在,我们来分析其关键部分:

2.2.1 页面状态管理
typescript 复制代码
@Entry
@ComponentV2
export struct ServicesPage {
  @Local userMode: AppMode = AppMode.HOME_GARDENING;  // 用户模式
  @Local nickname: string = '';                        // 用户昵称
  @Local showModeSheet: boolean = false;               // 是否显示模式切换弹窗
  @Local avatarUri: string = '';                       // 头像URI
  @Local tempSelectedMode: AppMode = AppMode.HOME_GARDENING; // 临时选择的模式

  private imageService: ImageService = ImageService.getInstance(); // 图片服务
}
2.2.2 生命周期方法
typescript 复制代码
async aboutToAppear(): Promise<void> {
  // 初始化图片服务
  const context = getContext(this) as common.UIAbilityContext;
  this.imageService.initialize(context);

  // 加载用户数据
  await this.loadUserData();
}

/**
 * 页面每次显示时刷新数据
 * 确保从其他页面返回时能看到最新的用户信息
 */
async onPageShow(): Promise<void> {
  await this.loadUserData();
}
2.2.3 加载用户数据
typescript 复制代码
/**
 * 加载用户数据
 */
async loadUserData(): Promise<void> {
  // 加载用户模式
  const mode = await StorageUtil.getString('user_mode', AppMode.HOME_GARDENING);
  this.userMode = mode as AppMode;
  this.tempSelectedMode = mode as AppMode;

  // 加载昵称
  const name = await StorageUtil.getString('user_nickname', '用户');
  this.nickname = name;

  // 加载头像
  const avatar = await StorageUtil.getString('user_avatar', '');
  this.avatarUri = avatar;
}

2.3 用户信息卡片实现

用户信息卡片是用户中心的核心展示区域:

typescript 复制代码
@Builder
buildUserInfo() {
  Column() {
    Row() {
      // 头像区域
      Stack() {
        // 头像或默认图标
        if (this.avatarUri) {
          Image(this.getImageUri(this.avatarUri))
            .width(80)
            .height(80)
            .borderRadius(40)
            .objectFit(ImageFit.Cover)
            .backgroundColor($r('app.color.background'))
            .onError(() => {
              console.error('[ServicesPage] Failed to load avatar image');
              this.avatarUri = '';
            })
        } else {
          Column() {
            Text('👤')
              .fontSize(48)
          }
          .width(80)
          .height(80)
          .justifyContent(FlexAlign.Center)
          .backgroundColor($r('app.color.background'))
          .borderRadius(40)
        }

        // 编辑图标提示
        Column() {
          Text('📷')
            .fontSize(16)
        }
        .width(24)
        .height(24)
        .backgroundColor('#80000000')
        .borderRadius(12)
        .justifyContent(FlexAlign.Center)
        .position({ x: 56, y: 56 })
      }
      .width(80)
      .height(80)
      .onClick(() => {
        this.showImagePickerMenu();
      })

      // 用户信息
      Column({ space: 6 }) {
        Text(this.nickname)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
        Text(this.userMode === AppMode.HOME_GARDENING ?
          '家庭园艺爱好者' : '专业农业从业者')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 16 })

      // 编辑按钮
      Button('编辑')
        .height(32)
        .fontSize(14)
        .backgroundColor($r('app.color.background'))
        .fontColor(this.userMode === AppMode.HOME_GARDENING ?
          $r('app.color.primary_home_gardening') :
          $r('app.color.primary_professional'))
        .borderRadius(16)
        .onClick(() => {
          router.pushUrl({ url: 'pages/Services/ProfileEditPage' });
        })
    }
    .width('100%')
  }
  .width('100%')
  .padding(20)
  .backgroundColor($r('app.color.card_background'))
  .borderRadius(16)
  .margin({ bottom: 16 })
  .shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
}

关键点说明

  1. 头像显示逻辑

    • 如果有头像URI,显示图片
    • 如果没有头像,显示默认的表情符号
    • 右下角显示相机图标提示可以更换
  2. 点击头像触发选择 :调用 showImagePickerMenu() 方法

  3. 编辑按钮:跳转到个人资料编辑页面

2.4 头像选择菜单实现

当用户点击头像时,弹出选择菜单:

typescript 复制代码
/**
 * 显示图片选择菜单
 */
private showImagePickerMenu(): void {
  AlertDialog.show({
    title: '选择头像',
    message: '请选择获取头像的方式',
    primaryButton: {
      value: '拍照',
      action: async () => {
        await this.takePicture();
      }
    },
    secondaryButton: {
      value: '从相册选择',
      action: async () => {
        await this.pickFromGallery();
      }
    },
    cancel: () => {
      console.info('取消选择');
    }
  });
}

/**
 * 拍照
 */
private async takePicture(): Promise<void> {
  try {
    const result = await this.imageService.takePicture();
    if (result.success && result.imageInfo) {
      await this.saveAvatar(result.imageInfo.uri);
    } else {
      promptAction.showToast({
        message: result.error || '拍照失败',
        duration: 2000
      });
    }
  } catch (error) {
    console.error('拍照失败:', error);
    promptAction.showToast({
      message: '拍照失败',
      duration: 2000
    });
  }
}

/**
 * 从相册选择
 */
private async pickFromGallery(): Promise<void> {
  try {
    const result = await this.imageService.pickFromGallery();
    if (result.success && result.imageInfo) {
      await this.saveAvatar(result.imageInfo.uri);
    } else {
      promptAction.showToast({
        message: result.error || '选择图片失败',
        duration: 2000
      });
    }
  } catch (error) {
    console.error('选择图片失败:', error);
    promptAction.showToast({
      message: '选择图片失败',
      duration: 2000
    });
  }
}

/**
 * 保存头像
 */
private async saveAvatar(uri: string): Promise<void> {
  try {
    await StorageUtil.saveString('user_avatar', uri);
    this.avatarUri = uri;
    promptAction.showToast({
      message: '头像更新成功',
      duration: 2000
    });
  } catch (error) {
    console.error('保存头像失败:', error);
    promptAction.showToast({
      message: '保存头像失败',
      duration: 2000
    });
  }
}

/**
 * 获取图片URI(处理file://前缀)
 */
private getImageUri(uri: string): string {
  if (!uri) return '';
  return uri.startsWith('file://') ? uri : `file://${uri}`;
}

2.5 账户设置区实现

typescript 复制代码
@Builder
buildAccountSettings() {
  Column() {
    Text('账户设置')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .width('100%')
      .margin({ bottom: 12 })

    this.buildSettingItem('📝', '个人信息', '管理您的个人资料', () => {
      router.pushUrl({ url: 'pages/Services/ProfileEditPage' });
    })

    this.buildSettingItem('🔄', '切换模式',
      this.userMode === AppMode.HOME_GARDENING ?
        '当前: 家庭园艺模式' : '当前: 专业农业模式', () => {
      this.showModeSwitchDialog();
    })

    this.buildSettingItem('🔔', '消息通知', '管理推送通知设置', () => {
      router.pushUrl({ url: 'pages/Services/NotificationSettingsPage' });
    })

    this.buildSettingItem('🔐', '隐私安全', '账号安全与隐私设置', () => {
      router.pushUrl({ url: 'pages/Services/PrivacySettingsPage' });
    })
  }
  .width('100%')
  .padding(16)
  .backgroundColor($r('app.color.card_background'))
  .borderRadius(16)
  .margin({ bottom: 16 })
}

2.6 设置项通用组件

typescript 复制代码
@Builder
buildSettingItem(icon: string, title: string, subtitle: string, onClick: () => void) {
  Row() {
    Text(icon)
      .fontSize(24)
      .width(40)

    Column({ space: 4 }) {
      Text(title)
        .fontSize(15)
        .fontWeight(FontWeight.Medium)
      Text(subtitle)
        .fontSize(13)
        .fontColor($r('app.color.text_secondary'))
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
    .margin({ left: 12 })

    Text('›')
      .fontSize(20)
      .fontColor($r('app.color.text_secondary'))
  }
  .width('100%')
  .padding({ top: 12, bottom: 12 })
  .onClick(onClick)
}

三、模式切换功能

3.1 模式切换弹窗

模式切换是用户中心的重要功能,允许用户在家庭园艺和专业农业模式之间切换:

typescript 复制代码
/**
 * 显示模式切换对话框
 */
private showModeSwitchDialog(): void {
  this.showModeSheet = true;
}

/**
 * 模式切换弹窗
 */
@Builder
buildModeSheet() {
  Column() {
    // 遮罩层
    Column()
      .width('100%')
      .layoutWeight(1)
      .backgroundColor('#80000000')
      .onClick(() => {
        this.showModeSheet = false;
      })

    // 底部弹窗内容
    Column({ space: 0 }) {
      // 标题
      Row() {
        Text('切换使用模式')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)

        Button('取消')
          .backgroundColor(Color.Transparent)
          .fontColor($r('app.color.text_secondary'))
          .onClick(() => {
            this.showModeSheet = false;
          })
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .borderRadius({ topLeft: 16, topRight: 16 })
      .backgroundColor($r('app.color.card_background'))

      Divider()

      // 提示信息
      Column({ space: 12 }) {
        Text('切换模式后,应用界面和功能会相应调整')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))
          .textAlign(TextAlign.Center)
          .width('100%')

        // 模式选项
        Column({ space: 12 }) {
          this.buildModeOption(
            '🏡 家庭园艺',
            '适合阳台、庭院等小规模种植',
            AppMode.HOME_GARDENING
          )

          this.buildModeOption(
            '🌾 专业农业',
            '适合农场、基地等大规模种植',
            AppMode.PROFESSIONAL_AGRICULTURE
          )
        }
        .width('100%')

        Button('确认切换')
          .width('100%')
          .height(48)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .backgroundColor($r('app.color.primary_home_gardening'))
          .fontColor(Color.White)
          .borderRadius(24)
          .enabled(this.userMode !== this.tempSelectedMode)
          .onClick(async () => {
            await this.switchMode();
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor($r('app.color.card_background'))
    }
    .width('100%')
  }
  .width('100%')
  .height('100%')
  .position({ x: 0, y: 0 })
}

3.2 模式选项组件

typescript 复制代码
@Builder
buildModeOption(title: string, desc: string, mode: AppMode) {
  Row() {
    Column({ space: 4 }) {
      Text(title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor($r('app.color.text_primary'))

      Text(desc)
        .fontSize(13)
        .fontColor($r('app.color.text_secondary'))
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)

    if (this.tempSelectedMode === mode) {
      Text('✓')
        .fontSize(24)
        .fontColor($r('app.color.primary_home_gardening'))
    }
  }
  .width('100%')
  .padding(16)
  .backgroundColor(this.tempSelectedMode === mode ? '#F0F9FF' : $r('app.color.background'))
  .borderRadius(8)
  .borderWidth(1)
  .borderColor(this.tempSelectedMode === mode ?
    $r('app.color.primary_home_gardening') : $r('app.color.border'))
  .onClick(() => {
    this.tempSelectedMode = mode;
  })
}

3.3 执行模式切换

typescript 复制代码
/**
 * 切换模式
 */
private async switchMode(): Promise<void> {
  try {
    await StorageUtil.saveString('user_mode', this.tempSelectedMode);
    this.userMode = this.tempSelectedMode;

    promptAction.showToast({
      message: '模式切换成功',
      duration: 2000
    });

    this.showModeSheet = false;

    // 延迟重启应用以应用新模式
    setTimeout(() => {
      router.replaceUrl({ url: 'pages/Index' });
    }, 500);
  } catch (error) {
    console.error('切换模式失败:', error);
    promptAction.showToast({
      message: '切换模式失败',
      duration: 2000
    });
  }
}

四、个人资料编辑页面

4.1 页面结构

ProfileEditPage 提供完整的个人资料编辑功能:

文件位置entry/src/main/ets/pages/Services/ProfileEditPage.ets

typescript 复制代码
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { StorageUtil } from '../../utils/StorageUtil';
import { AppMode } from '../../models/CommonModels';

@Entry
@ComponentV2
struct ProfileEditPage {
  @Local nickname: string = '';
  @Local email: string = '';
  @Local phone: string = '';
  @Local location: string = '';
  @Local bio: string = '';
  @Local userMode: AppMode = AppMode.HOME_GARDENING;
  @Local isSaving: boolean = false;

  async aboutToAppear(): Promise<void> {
    await this.loadProfile();
  }

  /**
   * 加载个人资料
   */
  async loadProfile(): Promise<void> {
    try {
      this.nickname = await StorageUtil.getString('user_nickname', '');
      this.email = await StorageUtil.getString('email', '');
      this.phone = await StorageUtil.getString('phone', '');
      this.location = await StorageUtil.getString('location', '');
      this.bio = await StorageUtil.getString('bio', '');
      const mode = await StorageUtil.getString('user_mode', AppMode.HOME_GARDENING);
      this.userMode = mode as AppMode;
    } catch (error) {
      console.error('Failed to load profile:', error);
    }
  }

  build() {
    Column() {
      // 头部
      this.buildHeader()

      // 表单内容
      Scroll() {
        Column({ space: 16 }) {
          // 基本信息
          this.buildBasicInfo()

          // 联系方式
          this.buildContactInfo()

          // 个人简介
          this.buildBioSection()

          // 使用模式
          this.buildModeSection()
        }
        .padding({ left: 16, right: 16, top: 16, bottom: 80 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Auto)

      // 底部保存按钮
      this.buildFooter()
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.background'))
  }

  @Builder
  buildHeader() {
    Row() {
      Button('< 返回')
        .backgroundColor(Color.Transparent)
        .fontColor($r('app.color.text_primary'))
        .onClick(() => {
          router.back();
        })

      Text('编辑资料')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
        .textAlign(TextAlign.Center)

      Text('     ')
        .width(60)
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor($r('app.color.card_background'))
  }

  @Builder
  buildBasicInfo() {
    Column({ space: 16 }) {
      Text('基本信息')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      Column({ space: 8 }) {
        Text('昵称')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))

        TextInput({ text: this.nickname, placeholder: '请输入昵称' })
          .onChange((value: string) => {
            this.nickname = value;
          })
      }

      Column({ space: 8 }) {
        Text('位置')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))

        TextInput({ text: this.location, placeholder: '如:北京市朝阳区' })
          .onChange((value: string) => {
            this.location = value;
          })
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  @Builder
  buildContactInfo() {
    Column({ space: 16 }) {
      Text('联系方式')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      Column({ space: 8 }) {
        Text('邮箱')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))

        TextInput({ text: this.email, placeholder: '请输入邮箱地址' })
          .type(InputType.Email)
          .onChange((value: string) => {
            this.email = value;
          })
      }

      Column({ space: 8 }) {
        Text('手机号')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))

        TextInput({ text: this.phone, placeholder: '请输入手机号' })
          .type(InputType.PhoneNumber)
          .onChange((value: string) => {
            this.phone = value;
          })
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  @Builder
  buildBioSection() {
    Column({ space: 16 }) {
      Text('个人简介')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      TextArea({ text: this.bio, placeholder: '介绍一下自己的种植经历和兴趣...' })
        .height(120)
        .onChange((value: string) => {
          this.bio = value;
        })
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  @Builder
  buildFooter() {
    Column() {
      Button(this.isSaving ? '保存中...' : '保存')
        .width('90%')
        .height(48)
        .fontSize(16)
        .enabled(!this.isSaving)
        .onClick(() => {
          this.saveProfile();
        })
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
  }

  /**
   * 保存个人资料
   */
  async saveProfile(): Promise<void> {
    if (!this.nickname.trim()) {
      promptAction.showToast({ message: '请输入昵称' });
      return;
    }

    this.isSaving = true;

    try {
      await StorageUtil.setString('user_nickname', this.nickname);
      await StorageUtil.setString('email', this.email);
      await StorageUtil.setString('phone', this.phone);
      await StorageUtil.setString('location', this.location);
      await StorageUtil.setString('bio', this.bio);

      promptAction.showToast({ message: '保存成功' });

      // 延迟返回,让用户看到提示
      setTimeout(() => {
        router.back();
      }, 500);
    } catch (error) {
      console.error('Failed to save profile:', error);
      promptAction.showToast({ message: '保存失败' });
    } finally {
      this.isSaving = false;
    }
  }
}

4.2 关键功能说明

4.2.1 表单验证

在保存前进行基本的表单验证:

typescript 复制代码
if (!this.nickname.trim()) {
  promptAction.showToast({ message: '请输入昵称' });
  return;
}
4.2.2 数据持久化

使用 StorageUtil 保存用户数据:

typescript 复制代码
await StorageUtil.setString('user_nickname', this.nickname);
await StorageUtil.setString('email', this.email);
// ... 其他字段
4.2.3 用户反馈

保存成功后显示提示并返回:

typescript 复制代码
promptAction.showToast({ message: '保存成功' });
setTimeout(() => {
  router.back();
}, 500);

五、实操练习

5.1 测试用户中心功能

  1. 启动应用,进入"服务与设置"页面
  2. 点击头像,测试图片选择功能
  3. 点击编辑按钮,进入资料编辑页面
  4. 修改个人信息,测试保存功能
  5. 测试模式切换,观察界面变化

5.2 扩展功能

可以尝试添加以下功能:

  1. 头像裁剪:选择图片后进行裁剪
  2. 数据验证:邮箱、手机号格式验证
  3. 成就系统:根据用户行为解锁成就
  4. 数据统计:展示用户的使用数据

六、常见问题

6.1 图片选择失败

问题:点击头像后无法选择图片

解决方案

  1. 检查权限配置(相机、相册权限)
  2. 确保 ImageService 已正确初始化
  3. 查看日志中的错误信息

6.2 数据保存失败

问题:修改资料后保存失败

解决方案

  1. 检查 StorageUtil 是否正常工作
  2. 确保数据格式正确
  3. 查看是否有异常抛出

6.3 模式切换不生效

问题:切换模式后界面没有变化

解决方案

  1. 确保使用了 @Local 装饰器
  2. 检查是否正确保存了模式数据
  3. 确认页面刷新机制是否正常

七、本篇小结

本篇教程实现了完整的用户中心功能,包括:

个人信息展示 :头像、昵称、身份等

资料编辑功能 :完整的表单编辑和保存

图片选择功能 :拍照和相册选择

模式切换功能 :支持双模式切换

数据持久化:使用 Preferences 存储用户数据

核心技术点

  • HarmonyOS 图片选择 API(CameraPicker、PhotoViewPicker)
  • 图片处理和压缩
  • 表单设计和数据验证
  • 本地数据存储
  • 页面间数据传递和刷新

下一篇预告

第29篇将实现数据管理与备份功能,包括数据导出、导入、备份和恢复等功能。


📖 参考资料

相关推荐
雨季6662 小时前
破界与共生:HarmonyOS原生应用生态全景图谱与PC时代三重变局
flutter·华为·harmonyos
一路阳光8512 小时前
华为mate80现在确实没有日日新了,看来华为是对鸿蒙6有信心了
华为·harmonyos
三掌柜6662 小时前
如何从一个开发者成为鸿蒙KOL
华为·harmonyos
哈基米~南北绿豆2 小时前
虚拟机体验:在Windows/Mac上运行鸿蒙PC开发环境
windows·macos·harmonyos
爱笑的眼睛112 小时前
学着学着 我就给这个 HarmonyOS 应用增加了些新技术
华为·ai·harmonyos
花花_12 小时前
HarmonyOS开发:字符串全栈实战手册
harmonyos·鸿蒙领航者计划
REDcker2 小时前
鸿蒙系统发展史与纯血鸿蒙详解
华为·harmonyos·鸿蒙·鸿蒙系统
图形达人2 小时前
Siggraph Asia 2025,鸿蒙方舟图形引擎绽放,开创空间化体验元年!
华为·harmonyos
森之鸟2 小时前
uniapp——配置鸿蒙环境,进行真机调试
华为·uni-app·harmonyos