HarmonyOS开发:用户中心个人中心

📌 核心要点:个人中心是用户的"大本营",页面设计、用户信息编辑、地址管理、设置与隐私四大模块,数据安全和隐私保护是底线。

背景与动机

你打开一个App,点"我的"——头像、昵称、订单、地址、设置……这就是个人中心。

看起来就是一堆入口?你试试。用户信息编辑要校验手机号、头像上传要裁剪压缩、地址管理要支持省市区三级联动、设置页要处理退出登录和清除缓存——每个功能都不难,但细节多到让人头秃。

更别提隐私合规。你的App收集了哪些用户数据?存在哪里?用户能删除吗?退出登录后数据怎么处理?这些问题不搞清楚,应用市场审核都过不了。

个人中心的核心不是"画页面",而是数据管理和隐私合规。每个功能都涉及用户敏感数据,处理不当就是安全事故。

核心原理

个人中心的核心是数据分层管理:用户基础信息、业务数据(订单/地址)、应用设置,三层数据独立管理、独立存储、独立更新。

渲染错误: Mermaid 渲染失败: Parse error on line 32: ... class A,main class B,C,D,mod ----------------------^ Expecting 'SPACE', 'AMP', 'COLON', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'NEWLINE'

数据存储策略

数据类型 存储位置 更新频率 安全等级
用户基础信息 服务端+本地缓存 高(敏感数据加密)
收货地址 服务端+本地缓存 高(含手机号和地址)
应用设置 本地Preferences
登录凭证 本地加密存储 最高(Token加密存储)

隐私合规要点

  • 数据最小化:只收集必要的数据
  • 知情同意:收集前告知用户并获取同意
  • 数据可删除:用户有权删除自己的数据
  • 退出登录:清除本地敏感数据,保留非敏感设置

代码实战

基础用法:个人中心页面

先搞定个人中心的主页面——用户信息展示和功能入口。

// UserCenterPage.ets — 个人中心页面
import { router } from '@kit.ArkUI'

// 用户信息
interface UserInfo {
  userId: string
  avatar: string
  nickname: string
  phone: string
  gender: string
  birthday: string
  isVerified: boolean    // 是否实名认证
}

// 功能入口项
interface MenuGroup {
  title: string
  items: MenuItem[]
}

interface MenuItem {
  id: string
  icon: Resource
  title: string
  subtitle?: string
  badge?: number         // 角标数字
  routePath: string
  showArrow: boolean
}

@Entry
@Component
struct UserCenterPage {
  @State userInfo: UserInfo | null = null
  @State orderBadges: Record<string, number> = {}

  aboutToAppear() {
    this.loadUserInfo()
  }

  build() {
    Column() {
      // 用户信息头部
      this.UserInfoHeader()

      // 订单入口
      this.OrderEntry()

      // 功能菜单
      Scroll() {
        Column() {
          ForEach(this.getMenuGroups(), (group: MenuGroup) => {
            this.MenuGroupSection(group)
          }, (group: MenuGroup) => group.title)
        }
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  // ========== 用户信息头部 ==========
  @Builder
  UserInfoHeader() {
    Row() {
      Image(this.userInfo?.avatar || $r('app.media.ic_avatar_default'))
        .width(64)
        .height(64)
        .borderRadius(32)
        .objectFit(ImageFit.Cover)

      Column() {
        Text(this.userInfo?.nickname || '未登录')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)

        if (this.userInfo?.phone) {
          Text(this.maskPhone(this.userInfo.phone))
            .fontSize(13)
            .fontColor('#CCFFFFFF')
            .margin({ top: 4 })
        }

        if (this.userInfo?.isVerified) {
          Row() {
            Image($r('app.media.ic_verified'))
              .width(14)
              .height(14)
              .fillColor('#FFD700')
            Text('已认证')
              .fontSize(11)
              .fontColor('#FFD700')
              .margin({ left: 4 })
          }
          .margin({ top: 4 })
        }
      }
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 16 })

      Blank()

      Image($r('app.media.ic_arrow_right'))
        .width(20)
        .height(20)
        .fillColor('#FFFFFF')
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 24, bottom: 20 })
    .linearGradient({
      direction: GradientDirection.Right,
      colors: [['#1DA1F2', 0], ['#0D7CC4', 1]]
    })
    .onClick(() => {
      if (!this.userInfo) {
        router.pushUrl({ url: 'pages/LoginPage' })
      } else {
        router.pushUrl({ url: 'pages/UserProfilePage' })
      }
    })
  }

  // ========== 订单入口 ==========
  @Builder
  OrderEntry() {
    Column() {
      Row() {
        Text('我的订单')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')

        Blank()

        Text('查看全部 >')
          .fontSize(13)
          .fontColor('#999999')
          .onClick(() => {
            router.pushUrl({ url: 'pages/OrderListPage' })
          })
      }
      .width('100%')

      Row() {
        this.OrderTabItem($r('app.media.ic_order_pending_pay'), '待付款', this.orderBadges['PENDING_PAYMENT'] || 0, 'pages/OrderListPage')
        this.OrderTabItem($r('app.media.ic_order_pending_ship'), '待发货', this.orderBadges['PENDING_SHIPMENT'] || 0, 'pages/OrderListPage')
        this.OrderTabItem($r('app.media.ic_order_pending_receive'), '待收货', this.orderBadges['PENDING_RECEIPT'] || 0, 'pages/OrderListPage')
        this.OrderTabItem($r('app.media.ic_order_completed'), '已完成', 0, 'pages/OrderListPage')
        this.OrderTabItem($r('app.media.ic_order_return'), '退换货', this.orderBadges['RETURN'] || 0, 'pages/OrderListPage')
      }
      .width('100%')
      .margin({ top: 16 })
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(8)
    .margin({ top: -16, left: 12, right: 12 })
  }

  @Builder
  OrderTabItem(icon: Resource, title: string, badge: number, route: string) {
    Column() {
      Stack() {
        Image(icon)
          .width(28)
          .height(28)
          .fillColor('#333333')

        if (badge > 0) {
          Text(badge > 99 ? '99+' : `${badge}`)
            .fontSize(9)
            .fontColor(Color.White)
            .backgroundColor('#FF4444')
            .borderRadius(8)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            .position({ x: 16, y: -6 })
        }
      }

      Text(title)
        .fontSize(12)
        .fontColor('#333333')
        .margin({ top: 6 })
    }
    .layoutWeight(1)
    .onClick(() => {
      router.pushUrl({ url: route })
    })
  }

  // ========== 菜单分组 ==========
  @Builder
  MenuGroupSection(group: MenuGroup) {
    Column() {
      ForEach(group.items, (item: MenuItem, index?: number) => {
        Row() {
          Image(item.icon)
            .width(22)
            .height(22)
            .fillColor('#666666')

          Column() {
            Text(item.title)
              .fontSize(15)
              .fontColor('#333333')

            if (item.subtitle) {
              Text(item.subtitle)
                .fontSize(12)
                .fontColor('#999999')
                .margin({ top: 2 })
            }
          }
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 12 })

          Blank()

          if (item.badge && item.badge > 0) {
            Text(`${item.badge}`)
              .fontSize(10)
              .fontColor(Color.White)
              .backgroundColor('#FF4444')
              .borderRadius(8)
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          }

          if (item.showArrow) {
            Image($r('app.media.ic_arrow_right'))
              .width(16)
              .height(16)
              .fillColor('#CCCCCC')
              .margin({ left: 4 })
          }
        }
        .width('100%')
        .height(52)
        .padding({ left: 16, right: 16 })
        .onClick(() => {
          router.pushUrl({ url: item.routePath })
        })

        if ((index ?? 0) < group.items.length - 1) {
          Divider().color('#F0F0F0').margin({ left: 50 })
        }
      }, (item: MenuItem) => item.id)
    }
    .width('100%')
    .backgroundColor(Color.White)
    .borderRadius(8)
    .margin({ top: 8, left: 12, right: 12 })
  }

  // ========== 菜单数据 ==========
  getMenuGroups(): MenuGroup[] {
    return [
      {
        title: '服务',
        items: [
          { id: 'coupon', icon: $r('app.media.ic_coupon'), title: '优惠券', subtitle: '3张可用', routePath: 'pages/CouponPage', showArrow: true },
          { id: 'favorite', icon: $r('app.media.ic_favorite'), title: '我的收藏', routePath: 'pages/FavoritePage', showArrow: true },
          { id: 'history', icon: $r('app.media.ic_history'), title: '浏览足迹', routePath: 'pages/BrowseHistoryPage', showArrow: true },
          { id: 'address', icon: $r('app.media.ic_address'), title: '收货地址', routePath: 'pages/AddressListPage', showArrow: true },
        ]
      },
      {
        title: '设置',
        items: [
          { id: 'notification', icon: $r('app.media.ic_notification'), title: '消息通知', routePath: 'pages/NotificationSettingPage', showArrow: true },
          { id: 'privacy', icon: $r('app.media.ic_privacy'), title: '隐私设置', routePath: 'pages/PrivacySettingPage', showArrow: true },
          { id: 'security', icon: $r('app.media.ic_security'), title: '账号安全', routePath: 'pages/AccountSecurityPage', showArrow: true },
          { id: 'about', icon: $r('app.media.ic_about'), title: '关于我们', subtitle: 'v1.0.0', routePath: 'pages/AboutPage', showArrow: true },
          { id: 'settings', icon: $r('app.media.ic_settings'), title: '设置', routePath: 'pages/SettingsPage', showArrow: true },
        ]
      }
    ]
  }

  // ========== 辅助方法 ==========
  maskPhone(phone: string): string {
    if (phone.length === 11) {
      return `${phone.substring(0, 3)}****${phone.substring(7)}`
    }
    return phone
  }

  loadUserInfo() {
    this.userInfo = {
      userId: 'user_001',
      avatar: 'https://picsum.photos/128/128?random=1',
      nickname: '鸿蒙开发者',
      phone: '13800138000',
      gender: '男',
      birthday: '1995-01-15',
      isVerified: true,
    }
    this.orderBadges = {
      'PENDING_PAYMENT': 2,
      'PENDING_SHIPMENT': 1,
      'PENDING_RECEIPT': 3,
    }
  }
}

进阶用法:用户信息编辑与地址管理

用户信息编辑涉及头像上传、表单校验;地址管理涉及省市区联动、默认地址设置。

// UserProfilePage.ets — 用户信息编辑
import { router } from '@kit.ArkUI'
import { photoAccessHelper } from '@kit.MediaLibraryKit'

@Entry
@Component
struct UserProfilePage {
  @State avatar: string = ''
  @State nickname: string = ''
  @State phone: string = ''
  @State gender: string = ''
  @State birthday: string = ''
  @State isVerified: boolean = false
  @State showGenderPicker: boolean = false
  @State showBirthdayPicker: boolean = false

  aboutToAppear() {
    this.loadUserProfile()
  }

  build() {
    Column() {
      // 顶部导航
      Row() {
        Image($r('app.media.ic_back')).width(24).height(24).fillColor('#333333')
          .onClick(() => { router.back() })
        Text('个人信息').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 12 })
        Blank()
        Text('保存').fontSize(16).fontColor('#1DA1F2').onClick(() => { this.saveProfile() })
      }
      .width('100%').height(48).padding({ left: 16, right: 16 }).backgroundColor(Color.White)

      // 信息列表
      List() {
        // 头像
        ListItem() {
          Row() {
            Text('头像').fontSize(15).fontColor('#333333')
            Blank()
            Image(this.avatar).width(48).height(48).borderRadius(24).objectFit(ImageFit.Cover)
            Image($r('app.media.ic_arrow_right')).width(16).height(16).fillColor('#CCCCCC').margin({ left: 8 })
          }
          .width('100%').height(64).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
          .onClick(() => { this.changeAvatar() })
        }

        // 昵称
        ListItem() {
          Row() {
            Text('昵称').fontSize(15).fontColor('#333333')
            Blank()
            Text(this.nickname).fontSize(15).fontColor('#999999')
            Image($r('app.media.ic_arrow_right')).width(16).height(16).fillColor('#CCCCCC').margin({ left: 8 })
          }
          .width('100%').height(52).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
          .onClick(() => { router.pushUrl({ url: 'pages/EditNicknamePage' }) })
        }

        // 手机号
        ListItem() {
          Row() {
            Text('手机号').fontSize(15).fontColor('#333333')
            Blank()
            Text(this.maskPhone(this.phone)).fontSize(15).fontColor('#999999')
            Image($r('app.media.ic_arrow_right')).width(16).height(16).fillColor('#CCCCCC').margin({ left: 8 })
          }
          .width('100%').height(52).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
        }

        // 性别
        ListItem() {
          Row() {
            Text('性别').fontSize(15).fontColor('#333333')
            Blank()
            Text(this.gender || '未设置').fontSize(15).fontColor('#999999')
            Image($r('app.media.ic_arrow_right')).width(16).height(16).fillColor('#CCCCCC').margin({ left: 8 })
          }
          .width('100%').height(52).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
          .onClick(() => { this.showGenderPicker = true })
        }
        .bindMenu($$this.showGenderPicker, ['男', '女', '保密'], (value: string) => {
          this.gender = value
          this.showGenderPicker = false
        })

        // 生日
        ListItem() {
          Row() {
            Text('生日').fontSize(15).fontColor('#333333')
            Blank()
            Text(this.birthday || '未设置').fontSize(15).fontColor('#999999')
            Image($r('app.media.ic_arrow_right')).width(16).height(16).fillColor('#CCCCCC').margin({ left: 8 })
          }
          .width('100%').height(52).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
        }

        // 实名认证
        ListItem() {
          Row() {
            Text('实名认证').fontSize(15).fontColor('#333333')
            Blank()
            Text(this.isVerified ? '已认证' : '未认证')
              .fontSize(15).fontColor(this.isVerified ? '#2E7D32' : '#FF4444')
            Image($r('app.media.ic_arrow_right')).width(16).height(16).fillColor('#CCCCCC').margin({ left: 8 })
          }
          .width('100%').height(52).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
        }
      }
      .layoutWeight(1)
      .divider({ strokeWidth: 0.5, color: '#F0F0F0', startMargin: 16 })
      .scrollBar(BarState.Off)
    }
    .width('100%').height('100%').backgroundColor('#F5F5F5')
  }

  async changeAvatar() {
    try {
      // 实际项目:调用系统图片选择器
      // const picker = new photoAccessHelper.PhotoViewPicker()
      // const result = await picker.select({ MIMEType: photoAccessHelper.PhotoViewMIMEType.IMAGE_TYPE, maxSelectNumber: 1 })
      // 上传头像到云存储
      // const url = await uploadAvatar(result.photoUris[0])
      // this.avatar = url
    } catch (error) {
      console.error(`[UserProfile] 更换头像失败: ${JSON.stringify(error)}`)
    }
  }

  async saveProfile() {
    // 保存用户信息
    console.info('[UserProfile] 保存用户信息')
    router.back()
  }

  maskPhone(phone: string): string {
    if (phone.length === 11) return `${phone.substring(0, 3)}****${phone.substring(7)}`
    return phone
  }

  loadUserProfile() {
    this.avatar = 'https://picsum.photos/128/128?random=1'
    this.nickname = '鸿蒙开发者'
    this.phone = '13800138000'
    this.gender = '男'
    this.birthday = '1995-01-15'
    this.isVerified = true
  }
}

完整示例:地址管理与设置页

把地址管理(增删改查、默认地址)和设置页(退出登录、清除缓存、隐私管理)串成完整链路。

// AddressListPage.ets — 收货地址管理
import { router } from '@kit.ArkUI'
import { promptAction } from '@kit.ArkUI'

// 地址数据
interface AddressItem {
  id: string
  name: string           // 收货人
  phone: string          // 手机号
  province: string       // 省
  city: string           // 市
  district: string       // 区
  detail: string         // 详细地址
  isDefault: boolean     // 是否默认
  tag?: string           // 标签:家/公司/学校
}

@Entry
@Component
struct AddressListPage {
  @State addressList: AddressItem[] = []
  @State isSelectMode: boolean = false  // 选择地址模式(从订单页进入)

  aboutToAppear() {
    const params = router.getParams() as Record<string, boolean>
    this.isSelectMode = params?.selectMode || false
    this.loadAddressList()
  }

  build() {
    Column() {
      // 顶部栏
      Row() {
        Image($r('app.media.ic_back')).width(24).height(24).fillColor('#333333')
          .onClick(() => { router.back() })
        Text('收货地址').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 12 })
        Blank()
      }
      .width('100%').height(48).padding({ left: 16, right: 16 }).backgroundColor(Color.White)

      if (this.addressList.length === 0) {
        Column() {
          Text('暂无收货地址')
            .fontSize(16).fontColor('#999999')
          Text('添加一个收货地址吧')
            .fontSize(13).fontColor('#CCCCCC').margin({ top: 8 })
        }
        .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
      } else {
        List() {
          ForEach(this.addressList, (addr: AddressItem) => {
            ListItem() {
              this.AddressCard(addr)
            }
            .swipeAction({ end: this.AddressSwipeActions(addr) })
          }, (addr: AddressItem) => addr.id)
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
        .divider({ strokeWidth: 0.5, color: '#F0F0F0' })
      }

      // 新增地址按钮
      Button('新增收货地址')
        .width('90%').height(44)
        .fontSize(16).fontColor(Color.White)
        .backgroundColor('#1DA1F2').borderRadius(22)
        .margin({ top: 12, bottom: 24 })
        .onClick(() => {
          router.pushUrl({ url: 'pages/EditAddressPage' })
        })
    }
    .width('100%').height('100%').backgroundColor('#F5F5F5')
  }

  @Builder
  AddressCard(addr: AddressItem) {
    Column() {
      Row() {
        Text(addr.name).fontSize(16).fontWeight(FontWeight.Medium).fontColor('#333333')
        Text(addr.phone).fontSize(14).fontColor('#999999').margin({ left: 12 })
        if (addr.tag) {
          Text(addr.tag)
            .fontSize(11).fontColor('#1DA1F2').backgroundColor('#E3F2FD')
            .borderRadius(2).padding({ left: 4, right: 4, top: 1, bottom: 1 })
            .margin({ left: 8 })
        }
        if (addr.isDefault) {
          Text('默认')
            .fontSize(11).fontColor('#FF4444').backgroundColor('#FFF0F0')
            .borderRadius(2).padding({ left: 4, right: 4, top: 1, bottom: 1 })
            .margin({ left: 4 })
        }
      }

      Text(`${addr.province}${addr.city}${addr.district} ${addr.detail}`)
        .fontSize(14).fontColor('#666666').margin({ top: 8 })
        .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })

      // 底部操作
      Row() {
        Row() {
          Checkbox()
            .select($$addr.isDefault)
            .onChange((checked: boolean) => {
              this.setDefault(addr.id)
            })
            .width(16).height(16)
          Text('默认地址').fontSize(12).fontColor('#999999').margin({ left: 4 })
        }

        Blank()

        Text('编辑').fontSize(13).fontColor('#1DA1F2')
          .onClick(() => {
            router.pushUrl({ url: 'pages/EditAddressPage', params: { addressId: addr.id } })
          })
        Text('删除').fontSize(13).fontColor('#FF4444').margin({ left: 16 })
          .onClick(() => { this.deleteAddress(addr.id) })
      }
      .width('100%').margin({ top: 12 })
    }
    .width('100%').padding(16).backgroundColor(Color.White)
    .onClick(() => {
      if (this.isSelectMode) {
        // 选择地址后返回
        router.back()
      }
    })
  }

  @Builder
  AddressSwipeActions(addr: AddressItem) {
    Button('删除')
      .fontSize(14).fontColor(Color.White)
      .backgroundColor('#FF4444').width(70).height('100%').borderRadius(0)
      .onClick(() => { this.deleteAddress(addr.id) })
  }

  setDefault(addressId: string) {
    this.addressList.forEach(a => a.isDefault = (a.id === addressId))
    this.addressList = [...this.addressList]
  }

  deleteAddress(addressId: string) {
    this.addressList = this.addressList.filter(a => a.id !== addressId)
  }

  loadAddressList() {
    this.addressList = [
      { id: '1', name: '张三', phone: '13800138000', province: '北京市', city: '北京市', district: '朝阳区', detail: 'xxx街道xxx小区1号楼101', isDefault: true, tag: '家' },
      { id: '2', name: '张三', phone: '13800138000', province: '北京市', city: '北京市', district: '海淀区', detail: 'xxx大厦8层', isDefault: false, tag: '公司' },
    ]
  }
}

// ========== 设置页 ==========
@Entry
@Component
struct SettingsPage {
  @State cacheSize: string = '23.5MB'
  @State isLogin: boolean = true

  build() {
    Column() {
      Row() {
        Image($r('app.media.ic_back')).width(24).height(24).fillColor('#333333')
          .onClick(() => { router.back() })
        Text('设置').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 12 })
      }
      .width('100%').height(48).padding({ left: 16 }).backgroundColor(Color.White)

      Scroll() {
        Column() {
          // 通用设置
          Column() {
            this.SettingItem('消息通知', '', true)
            Divider().color('#F0F0F0').margin({ left: 16 })
            this.SettingItem('深色模式', '', true)
            Divider().color('#F0F0F0').margin({ left: 16 })
            this.SettingItem('语言', '简体中文', true)
          }
          .width('100%').backgroundColor(Color.White).borderRadius(8).margin({ top: 8, left: 12, right: 12 })

          // 隐私与安全
          Column() {
            this.SettingItem('隐私权限', '', true)
            Divider().color('#F0F0F0').margin({ left: 16 })
            this.SettingItem('账号安全', '', true)
            Divider().color('#F0F0F0').margin({ left: 16 })
            this.SettingItem('个人信息收集清单', '', true)
            Divider().color('#F0F0F0').margin({ left: 16 })
            this.SettingItem('第三方信息共享清单', '', true)
          }
          .width('100%').backgroundColor(Color.White).borderRadius(8).margin({ top: 8, left: 12, right: 12 })

          // 其他
          Column() {
            this.SettingItem('清除缓存', this.cacheSize, true)
            Divider().color('#F0F0F0').margin({ left: 16 })
            this.SettingItem('关于', 'v1.0.0', true)
          }
          .width('100%').backgroundColor(Color.White).borderRadius(8).margin({ top: 8, left: 12, right: 12 })

          // 退出登录
          if (this.isLogin) {
            Button('退出登录')
              .width('90%').height(44)
              .fontSize(16).fontColor('#FF4444')
              .backgroundColor(Color.White).borderRadius(8)
              .margin({ top: 24 })
              .onClick(() => {
                this.logout()
              })
          }
        }
      }
      .layoutWeight(1)
    }
    .width('100%').height('100%').backgroundColor('#F5F5F5')
  }

  @Builder
  SettingItem(title: string, subtitle: string, showArrow: boolean) {
    Row() {
      Text(title).fontSize(15).fontColor('#333333')
      Blank()
      if (subtitle) {
        Text(subtitle).fontSize(14).fontColor('#999999').margin({ right: 4 })
      }
      if (showArrow) {
        Image($r('app.media.ic_arrow_right')).width(16).height(16).fillColor('#CCCCCC')
      }
    }
    .width('100%').height(52).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
  }

  async logout() {
    // 1. 清除登录凭证
    // 2. 清除本地敏感数据
    // 3. 断开WebSocket
    // 4. 跳转登录页
    this.isLogin = false
    router.replaceUrl({ url: 'pages/LoginPage' })
  }
}

踩坑与注意事项

坑1:头像上传裁剪

用户选了一张4000x3000的照片当头像,直接上传?10MB的图片,上传慢、显示也慢。

解决方案:选图后先裁剪成正方形,再压缩到200x200,最后上传。裁剪可以用Canvas API,压缩用Image Kit的packing能力。

坑2:省市区三级联动数据

地址选择器的省市区数据从哪来?写死在代码里?那每次行政区划变更都要发版。

解决方案:省市区数据从服务端获取,本地缓存。数据格式用{code, name, children}的树形结构。

坑3:默认地址逻辑

用户设置了新默认地址,旧默认地址要取消。但如果两个地址同时设为默认呢?

解决方案:设默认地址时,服务端先取消所有默认,再设置新的默认。客户端同步更新。

坑4:退出登录数据清理

退出登录后,本地还存着用户头像、昵称、地址——下一个登录的用户能看到上一个用户的数据。

解决方案:退出登录时清除所有本地敏感数据,包括:

  • 用户信息缓存
  • 登录Token
  • 购物车数据
  • 地址数据
  • 保留非敏感设置(如深色模式偏好)

坑5:手机号脱敏

手机号在页面上显示为138****8000,但传给服务端时必须是完整手机号。

解决方案:显示用脱敏版本,数据存储用完整版本。UI层做脱敏,数据层不做。

HarmonyOS 6适配说明

HarmonyOS 6对个人中心相关能力做了以下更新:

  1. PhotoViewPicker增强:图片选择器新增裁剪功能,选择头像时可以直接裁剪成正方形,不需要额外的裁剪组件。

  2. 安全存储增强@kit.BasicServicesKit新增了HUKS(Universal KeyStore)的安全存储能力,Token等敏感数据可以用硬件级加密存储,即使手机被root也无法读取。

  3. 隐私合规API:新增@kit.PrivacyKit,提供隐私合规检查能力。App可以一键检查收集了哪些数据、是否有用户授权、是否符合应用市场要求。

  4. 地址选择器组件:新增AddressPicker组件,内置省市区三级联动数据,支持搜索和最近使用记录。

  5. 退出登录优化@kit.AccountKit新增了统一退出登录接口,一次调用清除所有账号相关数据,不需要逐个清理。

总结

个人中心的核心不是"画页面",而是数据管理和隐私合规。每个功能都涉及用户敏感数据,处理不当就是安全事故。

核心记住三点:

  • 敏感数据加密存储,Token用HUKS硬件加密,手机号脱敏显示
  • 退出登录必须清理数据,本地敏感数据全清,保留非敏感设置
  • 隐私合规不是可选项,个人信息收集清单、第三方共享清单必须有
评估维度 说明
学习难度 ⭐⭐⭐ 功能多但每个都不复杂,隐私合规需要关注
使用频率 ⭐⭐⭐⭐⭐ 所有App都有个人中心
重要程度 ⭐⭐⭐⭐ 隐私合规不过关,应用市场审核都过不了

用户退出登录后,下一个登录的人能看到上一个用户的地址——这不是bug,这是安全事故。

Logo

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

更多推荐