RN for OpenHarmony AnimeHub项目实战:随机推荐页面开发

案例开源地址:https://atomgit.com/nutpi/Rn_openharmony_AnimeHub
随机推荐页是一个有趣的功能,每次访问都会随机展示一部动漫。这篇来讲随机推荐页的实现,重点是单条数据展示、收藏功能集成和刷新交互设计。
功能设计
随机推荐页需要实现以下功能:
- 随机获取 - 每次加载随机获取一部动漫
- 详情展示 - 展示动漫的封面、标题、评分、简介等
- 收藏功能 - 可以直接收藏当前动漫
- 换一个 - 点击按钮重新随机获取
- 错误处理 - 加载失败时显示重试按钮
这个页面和之前的列表页面完全不同,它只展示一条数据,但展示得更加详细。用户可以在这里快速了解一部动漫,决定是否要深入查看或收藏。
随机推荐是一种"发现"功能。当用户不知道看什么的时候,随机推荐可以给他们惊喜。这种功能在音乐、视频、阅读类应用中很常见,比如网易云音乐的"私人FM"。
获取屏幕尺寸
用于计算图片尺寸:
const { width } = Dimensions.get('window');
用途说明:
- 获取屏幕宽度
- 用于设置封面图的尺寸
Dimensions.get('window')返回屏幕的宽度和高度。我们只需要宽度,所以用解构语法{ width }提取。
为什么需要屏幕宽度?因为封面图要占满屏幕宽度,高度按比例计算。这样可以保证在不同尺寸的设备上都有好的显示效果。
状态定义
这个页面的状态比较简单:
const [anime, setAnime] = useState<Anime | null>(null);
const [loading, setLoading] = useState(true);
状态说明:
anime- 当前随机获取的动漫,可能为 nullloading- 加载状态
和列表页面不同,这里只有一条数据,所以用
Anime | null类型而不是Anime[]。初始值是 null,表示还没有加载数据。
没有分页相关的状态,因为每次只加载一条数据。这让页面逻辑变得非常简单。
收藏功能集成
从全局状态获取收藏相关的方法:
const { isFavorite, toggleFavorite } = useStore();
方法说明:
isFavorite(id)- 判断某个动漫是否已收藏toggleFavorite(anime)- 切换收藏状态
useStore是我们封装的全局状态 Hook,基于 Zustand 实现。收藏数据存储在全局状态中,所有页面都可以访问和修改。
为什么在这个页面集成收藏功能?因为随机推荐的场景很适合收藏。用户看到一部感兴趣的动漫,可以直接收藏,以后再看。如果要先进入详情页才能收藏,操作路径就太长了。
数据加载函数
加载随机动漫:
const loadRandom = async () => {
setLoading(true);
try {
const res = await getRandomAnime();
setAnime(res.data);
} catch (error) {
console.error('Load error:', error);
} finally {
setLoading(false);
}
};
加载逻辑:
- 先设置 loading 为 true
- 调用 API 获取随机动漫
- 成功时设置 anime 数据
- 失败时打印错误日志
- 最终重置 loading 状态
getRandomAnime是封装好的 API 函数。Jikan API 提供了随机获取动漫的接口,每次调用返回不同的结果。
注意这里没有用
setAnime(res.data || null),因为如果 API 返回空数据,anime 会是 undefined,后面的条件渲染会处理这种情况。
初始加载
useEffect(() => {
loadRandom();
}, []);
说明:
- 组件挂载时加载一次
- 依赖数组为空,只执行一次
和列表页面一样的模式。但这个页面还有一个特点:用户可以手动触发重新加载(点击"换一个"按钮),所以
loadRandom函数需要在组件外部可调用。
跳转详情页
const handleViewDetail = () => {
if (anime) {
navigation.navigate('AnimeDetail', { animeId: anime.mal_id });
}
};
逻辑说明:
- 检查 anime 是否存在
- 存在则跳转到详情页
为什么要检查
if (anime)?因为 anime 可能是 null(加载失败或还在加载中)。虽然在 UI 上,只有 anime 存在时才会显示"查看详情"按钮,但加一个检查更安全。
加载状态渲染
if (loading) {
return (
<View style={styles.container}>
<Header title="随机推荐" showBack onBack={() => navigation.goBack()} />
<Loading fullScreen text="正在为你挑选..." />
</View>
);
}
设计亮点:
- Loading 文字是"正在为你挑选…“而不是"加载中…”
- 更有趣味性,符合随机推荐的场景
文案的选择很重要。"正在为你挑选…"暗示系统在为用户精心挑选内容,比"加载中…"更有温度。这种小细节可以提升用户体验。
错误状态渲染
if (!anime) {
return (
<View style={styles.container}>
<Header title="随机推荐" showBack onBack={() => navigation.goBack()} />
<View style={styles.errorContainer}>
<Icon name="error" size={48} color={Colors.textMuted} />
<Text style={styles.errorText}>加载失败</Text>
<TouchableOpacity style={styles.retryButton} onPress={loadRandom}>
<Text style={styles.retryText}>重试</Text>
</TouchableOpacity>
</View>
</View>
);
}
错误处理:
- 显示错误图标和文字
- 提供重试按钮
- 点击重试重新加载
错误状态的处理很重要。网络请求可能失败,用户需要知道发生了什么,以及如何解决。提供重试按钮让用户可以自己尝试恢复。
注意这里的条件是
!anime而不是检查某个 error 状态。这是一种简化的处理方式:如果加载完成但 anime 为空,就认为是失败了。
收藏状态判断
const favorite = isFavorite(anime.mal_id);
说明:
- 调用全局状态的方法判断是否已收藏
- 结果用于控制收藏按钮的样式和文字
这行代码放在条件渲染之后,确保 anime 不为 null。如果放在前面,anime 为 null 时会报错。
Header 配置
<Header
title="随机推荐"
showBack
onBack={() => navigation.goBack()}
rightIcon="dice"
onRightPress={loadRandom}
/>
配置说明:
- 标题"随机推荐"
- 显示返回按钮
- 右侧显示骰子图标
- 点击骰子重新随机
骰子图标(dice)非常贴切,象征随机性。用户点击骰子就像掷骰子一样,会得到一个随机结果。这种隐喻让交互更直观。
在 Header 中放置刷新按钮是一个好的设计。用户不需要滚动到页面底部就能刷新,操作更便捷。
封面图展示
<View style={styles.imageContainer}>
<Image
source={{ uri: anime.images.jpg.large_image_url }}
style={styles.image}
resizeMode="cover"
/>
<View style={styles.overlay} />
{anime.score && (
<View style={styles.scoreContainer}>
<ScoreDisplay score={anime.score} size="lg" />
</View>
)}
</View>
布局结构:
- 外层容器控制尺寸
- Image 显示封面图
- overlay 是半透明遮罩层
- 评分显示在右下角
resizeMode="cover"让图片填满容器,可能会裁剪部分内容。这比contain(完整显示但可能有空白)更适合封面图展示。
遮罩层(overlay)的作用是让图片上的文字更容易阅读。纯图片上放白色文字可能看不清,加一层半透明黑色遮罩可以增加对比度。
封面图样式
imageContainer: {
width: width,
height: width * 1.2,
position: 'relative',
},
image: {
width: '100%',
height: '100%',
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.3)',
},
样式详解:
- 容器宽度等于屏幕宽度
- 高度是宽度的 1.2 倍(竖向长图)
position: 'relative'让子元素可以绝对定位- 图片填满容器
- 遮罩层用
absoluteFillObject覆盖整个容器
StyleSheet.absoluteFillObject是 React Native 提供的便捷样式,等价于{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }。
width * 1.2的比例是根据动漫海报的常见比例选择的。动漫海报通常是竖向的,宽高比大约是 2:3 或 3:4。1.2 的比例接近 5:6,显示效果不错。
内容区域
<View style={styles.content}>
<Text style={styles.title}>{anime.title}</Text>
{anime.title_japanese && (
<Text style={styles.japaneseTitle}>{anime.title_japanese}</Text>
)}
标题显示:
- 主标题(通常是英文或罗马音)
- 日文标题(如果有的话)
日文标题用条件渲染,因为不是所有动漫都有日文标题。有些动漫是非日本制作的,可能没有日文名。
content: {
padding: Spacing.lg,
marginTop: -Spacing.xxl,
backgroundColor: Colors.background,
borderTopLeftRadius: BorderRadius.xl,
borderTopRightRadius: BorderRadius.xl,
},
内容区域样式:
- 负的 marginTop 让内容区域向上覆盖图片
- 圆角让过渡更自然
- 背景色覆盖图片
这是一种常见的设计模式:内容区域向上延伸,覆盖部分图片,形成"卡片浮起"的效果。圆角增加了视觉层次感。
标签展示
<View style={styles.badges}>
{anime.type && <Badge text={anime.type} />}
{anime.status && <Badge text={anime.status} color={Colors.accent} />}
{anime.episodes && <Badge text={`${anime.episodes}集`} color={Colors.backgroundLight} />}
</View>
标签内容:
- 类型(TV、Movie、OVA 等)
- 状态(正在播出、已完结等)
- 集数
Badge 组件是封装好的标签组件,支持自定义颜色。不同类型的信息用不同颜色区分,视觉上更清晰。
分类标签
<View style={styles.genres}>
{anime.genres?.slice(0, 4).map(genre => (
<TouchableOpacity
key={genre.mal_id}
style={styles.genreTag}
onPress={() => navigation.navigate('GenreAnime', {
genreId: genre.mal_id,
genreName: genre.name
})}
>
<Text style={styles.genreText}>{genre.name}</Text>
</TouchableOpacity>
))}
</View>
分类标签功能:
- 最多显示 4 个分类
- 每个分类可点击
- 点击跳转到该分类的动漫列表
slice(0, 4)限制最多显示 4 个分类。有些动漫可能有很多分类标签,全部显示会占用太多空间。
分类标签可点击是一个很好的设计。用户看到感兴趣的分类,可以直接点击查看更多同类动漫,增加了内容发现的路径。
简介展示
{anime.synopsis && (
<Text style={styles.synopsis} numberOfLines={6}>
{anime.synopsis}
</Text>
)}
简介配置:
- 条件渲染,有简介才显示
- 最多显示 6 行,超出部分省略
numberOfLines={6}限制文本行数。简介可能很长,全部显示会占用太多空间。用户如果想看完整简介,可以点击"查看详情"进入详情页。
操作按钮
<View style={styles.actions}>
<TouchableOpacity
style={[styles.actionButton, styles.primaryButton]}
onPress={handleViewDetail}
>
<Icon name="info" size={20} />
<Text style={styles.actionText}>查看详情</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, favorite && styles.favoriteActive]}
onPress={() => toggleFavorite(anime)}
>
<Icon name={favorite ? 'heart-fill' : 'heart'} size={20} color={favorite ? Colors.accent : Colors.text} />
<Text style={[styles.actionText, favorite && styles.favoriteText]}>
{favorite ? '已收藏' : '收藏'}
</Text>
</TouchableOpacity>
</View>
两个操作按钮:
- 查看详情 - 跳转到详情页
- 收藏 - 切换收藏状态
收藏按钮的状态变化:
- 未收藏:空心爱心图标,文字"收藏"
- 已收藏:实心爱心图标,文字"已收藏",背景色变化
收藏按钮的视觉反馈很重要。用户点击后,图标、文字、颜色都会变化,让用户明确知道操作成功了。
换一个按钮
<TouchableOpacity style={styles.refreshButton} onPress={loadRandom}>
<Icon name="dice" size={24} />
<Text style={styles.refreshText}>换一个</Text>
</TouchableOpacity>
按钮设计:
- 骰子图标 + "换一个"文字
- 虚线边框,区别于其他按钮
- 点击重新加载随机动漫
refreshButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.sm,
paddingVertical: Spacing.lg,
backgroundColor: Colors.backgroundCard,
borderRadius: BorderRadius.lg,
borderWidth: 1,
borderColor: Colors.border,
borderStyle: 'dashed',
},
样式特点:
- 虚线边框(dashed)
- 较大的内边距
- 居中布局
虚线边框是一个视觉暗示,表示这是一个"可选"或"额外"的操作,而不是主要操作。主要操作(查看详情、收藏)用实心按钮,次要操作(换一个)用虚线按钮。
小结
随机推荐页是一个单条数据展示页面,和列表页面的设计思路完全不同。页面展示了动漫的封面、标题、评分、分类、简介等详细信息,用户可以直接收藏或查看详情。
"换一个"功能是这个页面的核心交互,用户可以通过 Header 的骰子图标或底部的按钮触发。骰子图标的隐喻让交互更直观有趣。
收藏功能的集成展示了如何在页面中使用全局状态。通过 useStore Hook 获取收藏相关的方法,可以判断收藏状态和切换收藏。
错误处理也是这个页面的亮点,加载失败时显示友好的错误提示和重试按钮,让用户可以自己尝试恢复。
下一篇会讲收藏页面,展示用户收藏的所有动漫。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)