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

这个功能要做什么

养护指南是狗狗之家里比较实用的一个模块。养狗的人都知道,从喂食到洗澡,从训练到看病,要学的东西挺多的。这个模块就是把这些知识整理好,让用户方便查阅。

和上一篇的趣味知识不同,养护指南涉及到两级页面:列表页展示分类,点击进入详情页看具体内容。这就涉及到页面间的参数传递,是个很常见的场景。

数据结构设计

先看数据是怎么组织的:

const CARE = [
  {icon: '🍖', title: '饮食营养', items: ['幼犬喂养', '成犬饮食', '老年犬营养', '禁忌食物']},
  {icon: '🛁', title: '日常护理', items: ['洗澡技巧', '毛发护理', '指甲修剪', '耳朵清洁']},

每个分类是一个对象,包含 icon(图标)、title(分类名)、items(子项数组)。

这种嵌套结构比扁平数组更清晰。如果把所有子项放在一个数组里,还得额外存储它属于哪个分类,麻烦。

继续看其他分类:

  {icon: '🏥', title: '健康医疗', items: ['疫苗接种', '常见疾病', '驱虫指南', '急救知识']},
  {icon: '🎾', title: '训练教育', items: ['基础训练', '行为纠正', '社会化', '技能训练']},

健康医疗和训练教育,都是养狗必须了解的内容。

  {icon: '🏠', title: '居家环境', items: ['狗窝选择', '安全防护', '玩具推荐', '出行准备']},
  {icon: '❤️', title: '情感陪伴', items: ['理解狗狗', '情绪识别', '互动游戏', '分离焦虑']},
];

居家环境和情感陪伴,这两个分类容易被忽视,但其实很重要。

一共 6 个分类,每个分类 4 个子项,总共 24 个知识点。

列表页的实现

CarePage 负责展示分类列表:

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

引入了 TouchableOpacity 处理点击,useNavigation 处理页面跳转。

组件函数开头获取导航方法:

export function CarePage() {
  const {navigate} = useNavigation();

useNavigation 返回一个对象,解构出 navigate 方法。后面点击子项时会用到。

页面结构:

  return (
    <View style={s.container}>
      <Header title="📖 养护指南" />
      <ScrollView style={s.content}>

外层容器 + Header + ScrollView,这个结构前几篇都见过了。

分类卡片的渲染

map 遍历分类数组:

        {CARE.map((c, i) => (
          <Card key={i}>
            <View style={s.head}>
              <Text style={s.icon}>{c.icon}</Text>
              <Text style={s.title}>{c.title}</Text>
            </View>

每个分类渲染一个 Card。卡片头部是图标 + 标题,横向排列。

子项列表的渲染:

            <View style={s.items}>
              {c.items.map((item, j) => (
                <TouchableOpacity 
                  key={j} 
                  style={s.item} 
                  onPress={() => navigate('CareDetail', {title: item, category: c.title})}
                >
                  <Text style={s.itemText}>{item}</Text>
                </TouchableOpacity>
              ))}
            </View>
          </Card>
        ))}

这里有个嵌套的 map:外层遍历分类,内层遍历子项。

点击子项时调用 navigate,传入两个参数:

  • title:子项名称,如"幼犬喂养"
  • category:所属分类,如"饮食营养"

这两个参数会传给详情页使用。

子项的布局技巧

子项要实现两列布局,看看样式怎么写的:

items: {flexDirection: 'row', flexWrap: 'wrap', marginHorizontal: -4},
item: {width: '50%', padding: 4},

关键点:

  • flexDirection: 'row' 让子项横向排列
  • flexWrap: 'wrap' 允许换行,一行放不下就换到下一行
  • width: '50%' 每个子项占一半宽度,所以一行正好两个

负边距技巧marginHorizontal: -4 配合子项的 padding: 4,可以让子项之间有间距,同时整体和卡片边缘对齐。

这是个常用的布局技巧,不用这个方法的话,第一个和最后一个子项会和边缘有多余的间距。

子项按钮的样式

itemText: {
  backgroundColor: '#f5f5f5', 
  paddingVertical: 10, 
  borderRadius: 8, 
  fontSize: 14, 
  color: '#666', 
  textAlign: 'center'
},
  • 灰色背景 #f5f5f5,和页面背景同色,但因为卡片是白色,所以能看出来
  • paddingVertical: 10 上下内边距,让按钮有一定高度
  • borderRadius: 8 圆角
  • textAlign: 'center' 文字居中

整体效果是一个个灰色的小按钮,点击有反馈。

卡片头部的样式

head: {flexDirection: 'row', alignItems: 'center', marginBottom: 12},
icon: {fontSize: 28, marginRight: 10},
title: {fontSize: 17, fontWeight: '600', color: '#333'},
  • 头部横向排列,垂直居中
  • 图标字号 28,比较大,醒目
  • 标题加粗,字号 17

marginBottom: 12 让头部和子项列表之间有间距。

详情页的参数接收

点击子项后跳转到 CareDetailPage,看看它怎么接收参数:

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

引入了 useRoute Hook,用来获取路由参数。

参数获取:

export function CareDetailPage() {
  const {params} = useRoute<{title: string; category: string}>();

useRoute 是泛型函数,传入参数类型 {title: string; category: string},这样 params 就有类型提示了。

useRoute 的实现原理

看看这个 Hook 是怎么写的:

export function useRoute<T = any>() {
  const [route, setRoute] = useState(navigator.getRoute());
  const [params, setParams] = useState<T>(navigator.getParams<T>());

用两个 state 分别存储当前路由名和参数。初始值从 navigator 获取。

订阅路由变化:

  useEffect(() => {
    const unsubscribe = navigator.subscribe((r, p) => {
      setRoute(r);
      setParams((p || {}) as T);
    });
    return unsubscribe;
  }, []);

  return {route, params};
}

当路由变化时,更新 state。p || {} 是兜底,防止参数为 undefined。

返回 {route, params},组件里解构使用。

详情内容的数据

详情页的内容也是写死的:

const CONTENT: Record<string, string> = {
  '幼犬喂养': '幼犬需要高蛋白、高能量的食物来支持快速生长。建议每天喂食3-4次,选择专门的幼犬粮。',
  '成犬饮食': '成年犬每天喂食1-2次即可。根据体型选择合适的狗粮,保持新鲜饮水。',

Record<string, string> 类型,key 是子项名称,value 是内容文本。

这种字典结构查找很方便,直接 CONTENT[params.title] 就能拿到对应内容。

更多内容:

  '禁忌食物': '狗狗不能吃:巧克力、葡萄、洋葱、大蒜、木糖醇、酒精、咖啡因等。',
  '洗澡技巧': '一般每2-4周洗一次澡。使用温水和狗狗专用沐浴露,注意保护眼睛和耳朵。',
  '毛发护理': '根据毛发类型选择合适的梳子,长毛犬需要每天梳理,短毛犬每周2-3次。',

每条内容都是实用的养护知识,不是随便写的。

训练相关的内容:

  '基础训练': '从简单的命令开始,如"坐下"、"趴下"。使用正向强化,用零食奖励。',
  '行为纠正': '对于不良行为,要及时纠正但不要惩罚。通过转移注意力和正向引导来改变。',
  '社会化': '在幼犬期进行社会化训练非常重要。让狗狗接触不同的人、动物和环境。',

强调正向训练,不提倡惩罚,这是现代训犬的主流理念。

详情页的渲染

export function CareDetailPage() {
  const {params} = useRoute<{title: string; category: string}>();
  const content = CONTENT[params.title] || '内容整理中...';

从字典里取内容,如果没找到就显示"内容整理中…"。这个兜底很重要,万一数据没对上,页面不会崩。

页面结构:

  return (
    <View style={s.container}>
      <Header title={params.title} />
      <ScrollView style={s.content}>
        <Card>
          <Text style={s.category}>{params.category}</Text>
          <Text style={s.title}>{params.title}</Text>
          <Text style={s.text}>{content}</Text>
        </Card>
      </ScrollView>
    </View>
  );
}

Header 的标题用参数里的 title,这样每个详情页的标题都不一样。

Card 里面三行内容:分类名、标题、正文。

详情页的样式

const s = StyleSheet.create({
  container: {flex: 1, backgroundColor: '#f5f5f5'},
  content: {flex: 1},
  category: {fontSize: 13, color: '#D2691E', marginBottom: 6},

分类名用主题色 #D2691E,字号小一点,起到标签的作用。

  title: {fontSize: 20, fontWeight: '600', color: '#333', marginBottom: 14},
  text: {fontSize: 15, color: '#666', lineHeight: 26},
});

标题字号 20,加粗,是页面的视觉重点。

正文 lineHeight: 26 行高比较大,阅读长文本时更舒服。

页面跳转的参数传递

回顾一下参数传递的完整流程:

发送方(CarePage):

navigate('CareDetail', {title: item, category: c.title})

第一个参数是目标路由名,第二个参数是要传递的数据对象。

接收方(CareDetailPage):

const {params} = useRoute<{title: string; category: string}>();

通过 useRoute 获取参数,泛型指定参数类型。

这种模式在 App 开发中非常常见,列表页传 ID 或数据给详情页,详情页根据参数渲染内容。

为什么不用接口

这个模块的数据全部写死在代码里,没有用接口。原因和趣味知识页面类似:

  • 内容相对固定,养护知识不会频繁变化
  • 减少网络依赖,离线也能用
  • 简化开发,不用处理加载和错误状态

如果后续内容越来越多,或者想做个性化推荐,再考虑接口化。

嵌套 map 的注意事项

CarePage 里用了嵌套的 map:

{CARE.map((c, i) => (
  <Card key={i}>
    ...
    {c.items.map((item, j) => (
      <TouchableOpacity key={j}>

两层 map 都需要 key。外层用 i,内层用 j,不会冲突,因为它们在不同的层级。

如果数据有唯一 ID,用 ID 做 key 更好。这里数据是静态的,用索引没问题。

两列布局的其他实现方式

除了 flexWrap,还有其他方式实现两列布局:

FlatList 的 numColumns

<FlatList data={items} numColumns={2} renderItem={...} />

FlatList 原生支持多列,但这里子项数量少,没必要用。

手动分组

把数组按两个一组拆分,然后渲染每组。代码会复杂一些,一般不推荐。

flexWrap 是最简单直接的方式,推荐使用。

可以优化的地方

当前实现能用,但有改进空间:

内容太短:每个知识点只有一两句话,可以扩充成更详细的文章。

缺少图片:纯文字有点枯燥,加些配图会更好。

没有搜索:内容多了之后,用户可能想搜索特定主题。

没有收藏:用户可能想收藏常看的内容。

这些后续可以慢慢加,先把核心功能做好。

小结

养护指南模块涉及到两级页面和参数传递,是 App 开发中很典型的场景。

关键点回顾:

  • 嵌套数据结构的设计,分类包含子项
  • flexWrap 实现两列布局
  • navigate 传递参数,useRoute 接收参数
  • 字典结构存储详情内容,方便查找

下一篇讲养护详情页的扩展,会加入更多内容和交互。


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

Logo

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

更多推荐