第28篇:用户中心与个人资料
📚 本篇导读
用户中心是应用的重要功能模块,为用户提供个人信息管理、账号设置、数据统计等功能。本篇教程将实现一个完整的用户中心系统,包括个人资料编辑、头像管理、模式切换等核心功能。
本篇将实现:
- 👤 个人资料管理(昵称、头像、联系方式、个人简介)
- 📸 头像上传功能(拍照、相册选择、图片处理)
- ⚙️ 账号设置功能(模式切换、通知设置、隐私安全)
- 📊 数据管理(缓存清理、数据分析)
- ℹ️ 关于与帮助(使用帮助、关于我们)
🎯 学习目标
完成本篇教程后,你将掌握:
- 如何设计用户中心页面布局
- 如何实现个人资料编辑功能
- 如何使用相机和相册选择图片
- 如何处理和存储图片
- 如何实现模式切换功能
- 用户数据的本地存储和管理
一、功能架构设计
1.1 用户中心功能结构
用户中心(ServicesPage)
├── 个人信息区
│ ├── 用户头像(可点击更换)
│ ├── 用户昵称
│ ├── 用户身份(家庭园艺/专业农业)
│ └── 编辑按钮
│
├── 账户设置区
│ ├── 个人信息(跳转到编辑页面)
│ ├── 切换模式(弹窗选择)
│ ├── 消息通知
│ └── 隐私安全
│
├── 应用设置区
│ ├── 数据管理
│ └── 数据分析
│
└── 关于区
├── 使用帮助
└── 关于我们
1.2 页面关系图
ServicesPage(服务与设置)
├─→ ProfileEditPage(编辑资料)
├─→ NotificationSettingsPage(消息通知)
├─→ PrivacySettingsPage(隐私安全)
├─→ DataManagementPage(数据管理)
├─→ DataAnalysisPage(数据分析)
├─→ HelpPage(使用帮助)
└─→ AboutPage(关于我们)
二、实现服务与设置页面
2.1 页面结构分析
ServicesPage 是用户中心的主页面,包含以下几个部分:
- 顶部导航栏:显示"服务与设置"标题
- 用户信息卡片:展示头像、昵称、身份,提供编辑入口
- 账户设置区:个人信息、模式切换、通知、隐私等
- 应用设置区:数据管理、数据分析
- 关于区:帮助、关于我们
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 })
}
关键点说明:
-
头像显示逻辑:
- 如果有头像URI,显示图片
- 如果没有头像,显示默认的表情符号
- 右下角显示相机图标提示可以更换
-
点击头像触发选择 :调用
showImagePickerMenu()方法 -
编辑按钮:跳转到个人资料编辑页面
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 测试用户中心功能
- 启动应用,进入"服务与设置"页面
- 点击头像,测试图片选择功能
- 点击编辑按钮,进入资料编辑页面
- 修改个人信息,测试保存功能
- 测试模式切换,观察界面变化
5.2 扩展功能
可以尝试添加以下功能:
- 头像裁剪:选择图片后进行裁剪
- 数据验证:邮箱、手机号格式验证
- 成就系统:根据用户行为解锁成就
- 数据统计:展示用户的使用数据
六、常见问题
6.1 图片选择失败
问题:点击头像后无法选择图片
解决方案:
- 检查权限配置(相机、相册权限)
- 确保 ImageService 已正确初始化
- 查看日志中的错误信息
6.2 数据保存失败
问题:修改资料后保存失败
解决方案:
- 检查 StorageUtil 是否正常工作
- 确保数据格式正确
- 查看是否有异常抛出
6.3 模式切换不生效
问题:切换模式后界面没有变化
解决方案:
- 确保使用了
@Local装饰器 - 检查是否正确保存了模式数据
- 确认页面刷新机制是否正常
七、本篇小结
本篇教程实现了完整的用户中心功能,包括:
✅ 个人信息展示 :头像、昵称、身份等
✅ 资料编辑功能 :完整的表单编辑和保存
✅ 图片选择功能 :拍照和相册选择
✅ 模式切换功能 :支持双模式切换
✅ 数据持久化:使用 Preferences 存储用户数据
核心技术点:
- HarmonyOS 图片选择 API(CameraPicker、PhotoViewPicker)
- 图片处理和压缩
- 表单设计和数据验证
- 本地数据存储
- 页面间数据传递和刷新
下一篇预告 :
第29篇将实现数据管理与备份功能,包括数据导出、导入、备份和恢复等功能。