案例开源地址:https://atomgit.com/nutpi/rn_openharmony_dogimg
请添加图片描述

用户的专属空间

个人中心是 App 里用户的"家"。在这里,用户可以看到自己的收藏、调整设置、了解 App 信息。

虽然狗狗之家目前没有登录功能,但个人中心依然重要。它展示用户的使用数据,提供各种功能入口,是 App 不可或缺的一部分。

这篇文章讲 ProfilePage 的实现,涉及到用户信息展示、数据统计、菜单列表等内容。

页面的导入部分

import React from 'react';
import {View, Text, ScrollView, TouchableOpacity, StyleSheet} from 'react-native';
import {Header, Card} from '../../components';
import {useNavigation, useStore} from '../../hooks';

导入了 ScrollView,因为个人中心的内容可能超出一屏。

Card 组件用来包裹菜单列表,提供统一的卡片样式。

useStore 获取全局状态,这里主要是收藏数据。

获取导航和状态

export function ProfilePage() {
  const {navigate} = useNavigation();
  const {favoriteBreeds, favoriteImages} = useStore();

navigate 用于跳转到各个子页面。

favoriteBreedsfavoriteImages 是用户收藏的品种和图片,用于显示统计数字。

这两个数组存在全局状态里,用户在其他页面收藏的内容,这里能实时看到数量变化。

页面整体结构

return (
  <View style={s.container}>
    <Header title="我的" showBack={false} />
    <ScrollView style={s.content}>
      <View style={s.userSection}>...</View>
      <View style={s.stats}>...</View>
      <Card>...</Card>
      <Card>...</Card>
    </ScrollView>
  </View>
);

页面分成几个区域:

Header:顶部导航栏,showBack={false} 因为这是底部 Tab 页面。

userSection:用户信息区,显示头像、昵称、签名。

stats:统计区,显示收藏数量。

Card:菜单列表,分成两组。

用户信息区

<View style={s.userSection}>
  <View style={s.avatar}>
    <Text style={s.avatarEmoji}>🐕</Text>
  </View>
  <Text style={s.userName}>狗狗爱好者</Text>
  <Text style={s.userDesc}>热爱狗狗,关爱生命</Text>
</View>

没有登录功能,所以用默认的头像和昵称。

头像用 emoji 代替图片,简单直接。🐕 狗狗符号和 App 主题呼应。

昵称"狗狗爱好者"是个通用的称呼,签名"热爱狗狗,关爱生命"传达 App 的理念。

用户信息区样式

userSection: {
  backgroundColor: '#D2691E', 
  padding: 24, 
  alignItems: 'center'
},

用主题色做背景,让这个区域成为页面的视觉焦点

padding: 24 给内容足够的呼吸空间。

alignItems: 'center' 让头像、昵称、签名都居中。

avatar: {
  width: 70, 
  height: 70, 
  borderRadius: 35, 
  backgroundColor: 'rgba(255,255,255,0.2)', 
  alignItems: 'center', 
  justifyContent: 'center', 
  marginBottom: 10
},
avatarEmoji: {fontSize: 36},

头像是个圆形容器,borderRadius: 35 是宽高的一半,形成正圆。

rgba(255,255,255,0.2) 是半透明白色,在主题色背景上形成柔和的对比。

emoji 用 36 号字,在 70x70 的容器里大小合适。

userName: {fontSize: 18, fontWeight: '600', color: '#fff'},
userDesc: {fontSize: 13, color: '#fff', opacity: 0.8, marginTop: 4},

昵称用白色加粗字体,在深色背景上清晰可见。

签名用小一号的字体,opacity: 0.8 让它比昵称稍暗,形成主次关系。

统计区

<View style={s.stats}>
  <TouchableOpacity style={s.statItem} onPress={() => navigate('Favorites')}>
    <Text style={s.statValue}>{favoriteBreeds.length}</Text>
    <Text style={s.statLabel}>收藏品种</Text>
  </TouchableOpacity>
  <TouchableOpacity style={s.statItem} onPress={() => navigate('Favorites')}>
    <Text style={s.statValue}>{favoriteImages.length}</Text>
    <Text style={s.statLabel}>收藏图片</Text>
  </TouchableOpacity>
</View>

两个统计项:收藏的品种数和图片数。

favoriteBreeds.lengthfavoriteImages.length 获取数组长度,这是实时数据。

点击任意一个都跳转到收藏页面,用户可以查看具体收藏了什么。

统计区样式

stats: {
  flexDirection: 'row', 
  backgroundColor: '#fff', 
  paddingVertical: 16, 
  marginBottom: 8
},
statItem: {flex: 1, alignItems: 'center'},

flexDirection: 'row' 让两个统计项横向排列。

flex: 1 让它们平分宽度。

alignItems: 'center' 让数字和标签居中。

白色背景和上面的主题色背景形成对比,视觉上分层。

statValue: {fontSize: 22, fontWeight: 'bold', color: '#D2691E'},
statLabel: {fontSize: 12, color: '#666', marginTop: 4},

数字用主题色和大号加粗字体,是统计区的焦点。

标签用小号灰色字体,说明数字的含义。

菜单列表的数据驱动渲染

<Card>
  {[
    {icon: '❤️', label: '我的收藏', route: 'Favorites'},
    {icon: '⚙️', label: '设置', route: 'Settings'},
  ].map((item, i) => (
    <TouchableOpacity key={i} style={s.menuItem} onPress={() => navigate(item.route)}>
      <Text style={s.menuIcon}>{item.icon}</Text>
      <Text style={s.menuLabel}>{item.label}</Text>
      <Text style={s.menuArrow}></Text>
    </TouchableOpacity>
  ))}
</Card>

用数组存储菜单数据,然后用 map 渲染。

这种数据驱动的写法比写多个重复的 TouchableOpacity 更简洁。要加新菜单项,只需要往数组里加一个对象。

每个菜单项有三个属性:icon(emoji 图标)、label(文字标签)、route(跳转路由)。

第二组菜单

<Card>
  {[
    {icon: '📖', label: '关于我们', route: 'About'},
    {icon: '⭐', label: '给个好评', route: 'Rate'},
    {icon: '📞', label: '联系我们', route: 'Feedback'},
  ].map((item, i) => (
    <TouchableOpacity key={i} style={s.menuItem} onPress={() => navigate(item.route)}>
      <Text style={s.menuIcon}>{item.icon}</Text>
      <Text style={s.menuLabel}>{item.label}</Text>
      <Text style={s.menuArrow}></Text>
    </TouchableOpacity>
  ))}
</Card>

第二组是"关于"类的菜单,和第一组的"功能"类菜单分开。

分组让菜单更有条理,用户能快速找到想要的功能。

两个 Card 之间有间距(Card 组件自带 marginVertical),视觉上分隔两组。

菜单项样式

menuItem: {
  flexDirection: 'row', 
  alignItems: 'center', 
  paddingVertical: 14, 
  borderBottomWidth: 1, 
  borderBottomColor: '#f0f0f0'
},

flexDirection: 'row' 让图标、标签、箭头横向排列。

paddingVertical: 14 给菜单项足够的点击区域。

底部有条细线分隔,最后一项的线会被 Card 的圆角裁掉,所以不用特殊处理。

menuIcon: {fontSize: 20, marginRight: 12},
menuLabel: {flex: 1, fontSize: 16, color: '#333'},
menuArrow: {fontSize: 18, color: '#ccc'},

图标 20 号字,和标签之间有 12 的间距。

标签 flex: 1 占据中间所有空间,把箭头挤到右边。

箭头用浅灰色,不抢眼但能提示"可点击进入"。

为什么用 emoji 做图标

项目里大量使用 emoji 作为图标,原因有几个:

简单:不需要引入图标库或图片资源。

跨平台:emoji 在 iOS、Android、鸿蒙上都能显示。

表达力强:一个符号就能传达含义,❤️ 是收藏,⚙️ 是设置,一目了然。

体积小:不增加包体积。

当然,emoji 的缺点是样式不可控,不同系统显示效果可能略有差异。如果对视觉一致性要求高,还是用图标库或自定义图片。

页面容器样式

container: {flex: 1, backgroundColor: '#f5f5f5'},
content: {flex: 1},

灰色背景,和其他页面保持一致。

contentflex: 1 让 ScrollView 占据 Header 之外的所有空间。

全局状态的来源

收藏数据存在 appStore 里:

export interface AppState {
  favoriteBreeds: number[];
  favoriteImages: string[];
  darkMode: boolean;
}

export const appStore = createStore<AppState>({
  favoriteBreeds: [],
  favoriteImages: [],
  darkMode: false,
});

favoriteBreeds 存品种 ID,是数字数组。

favoriteImages 存图片 ID,是字符串数组(The Dog API 的图片 ID 是字符串)。

darkMode 是深色模式开关,后面的文章会讲。

createStore 的实现

function createStore<T extends object>(initial: T) {
  let state = initial;
  const listeners = new Set<Listener<T>>();

  return {
    getState: () => state,
    setState: (partial: Partial<T>) => {
      state = {...state, ...partial};
      listeners.forEach(fn => fn(state));
    },
    subscribe: (fn: Listener<T>): (() => void) => {
      listeners.add(fn);
      return () => {
        listeners.delete(fn);
      };
    },
  };
}

这是个简单的状态管理实现。

getState 返回当前状态。

setState 接收部分状态,合并到现有状态,然后通知所有订阅者。

subscribe 添加订阅者,返回取消订阅的函数。

Partial<T> 是 TypeScript 的工具类型,让 T 的所有属性变成可选。这样 setState 时不用传完整的状态对象。

useStore Hook 的工作原理

export function useStore(): AppState & {...} {
  const [state, setState] = useState(appStore.getState());

  useEffect(() => {
    const unsubscribe = appStore.subscribe(setState);
    return unsubscribe;
  }, []);

  // ...
  return {...state, toggleBreed, toggleImage};
}

组件挂载时订阅状态变化,状态变化时调用 setState 触发重渲染。

组件卸载时取消订阅,避免内存泄漏。

这样任何组件调用 useStore,都能获取最新状态,状态变化时自动更新。

扩展:登录功能

如果要加登录功能,用户信息区会变成:

{user ? (
  <View style={s.userSection}>
    <Image source={{uri: user.avatar}} style={s.avatarImg} />
    <Text style={s.userName}>{user.nickname}</Text>
    <Text style={s.userDesc}>{user.bio}</Text>
  </View>
) : (
  <TouchableOpacity style={s.loginSection} onPress={() => navigate('Login')}>
    <Text style={s.loginText}>点击登录</Text>
  </TouchableOpacity>
)}

根据是否有 user 数据,显示用户信息或登录入口。

登录后头像变成真实图片,昵称和签名也是用户自己设置的。

扩展:更多统计

可以加更多统计项:

<View style={s.stats}>
  <StatItem value={favoriteBreeds.length} label="收藏品种" />
  <StatItem value={favoriteImages.length} label="收藏图片" />
  <StatItem value={adoptApplications.length} label="领养申请" />
  <StatItem value={votedImages.length} label="投票图片" />
</View>

展示用户在 App 里的各种活动数据,增加成就感。

扩展:会员等级

<View style={s.levelSection}>
  <Text style={s.levelIcon}>🏆</Text>
  <Text style={s.levelName}>狗狗达人</Text>
  <View style={s.levelProgress}>
    <View style={[s.levelBar, {width: '60%'}]} />
  </View>
  <Text style={s.levelTip}>再收藏 10 个品种升级为"狗狗专家"</Text>
</View>

根据用户的活跃度设置等级,用进度条显示升级进度。

游戏化的设计能提升用户粘性。

小结

个人中心页面的实现要点:

  • 用户信息区:主题色背景,头像居中,形成视觉焦点
  • 统计区:展示收藏数量,点击可跳转详情
  • 菜单列表:数据驱动渲染,分组展示
  • 全局状态:用 createStore 实现简单的状态管理
  • emoji 图标:简单跨平台,表达力强

个人中心是用户了解自己使用情况的窗口,设计好了能提升用户的归属感。

下一篇讲我的收藏页面,展示用户收藏的品种和图片。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐