RN for OpenHarmony AnimeHub项目实战:人气排行页面开发

案例开源地址:https://atomgit.com/nutpi/Rn_openharmony_AnimeHub

评分高 ≠ 人气高

在动漫圈有个有趣的现象:评分最高的作品和最火的作品往往不是同一批。

评分榜前几名通常是《钢之炼金术师》《银魂》这样的经典老番,它们经过时间检验,口碑极佳。但要说"人气",可能当季的热门新番更高,因为正在播出,讨论度高,观看人数多。

所以 AnimeHub 提供了两个不同的排行:

  • 热门排行(按评分)- 质量最好的作品
  • 人气排行(按人气)- 最多人看的作品

这篇讲的是人气排行页面。

人气是怎么算的

MyAnimeList 的人气(popularity)是根据"有多少用户把这部动漫加入了自己的列表"来计算的。不管是"想看"、"在看"还是"看过",只要加入列表就算。

这个指标反映的是"知名度"和"关注度",而不是"好不好看"。一部作品可能评分一般,但因为话题性强、宣传到位,人气很高。

一分钟看完核心代码

人气排行页面的实现非常简单,和热门排行页面几乎一样,只是 API 参数不同:

typescript 复制代码
// 热门排行(按评分)
const res = await getTopAnime(pageNum, '');

// 人气排行(按人气)
const res = await getTopAnime(pageNum, 'bypopularity');

就这一个参数的区别。'bypopularity' 告诉 API 按人气排序而不是按评分排序。

逐段解析

导入和类型

typescript 复制代码
import React, { useEffect, useState, useCallback } from 'react';
import { View, FlatList, StyleSheet } from 'react-native';
import { Colors, Spacing } from '../../theme';
import { Anime } from '../../types';
import { getTopAnime } from '../../api/jikan';
import { AnimeListItem } from '../../components/anime';
import { Header, Loading, EmptyState } from '../../components/common';

没什么特别的,标准的分页列表页面导入。

组件和状态

typescript 复制代码
export const PopularAnimeScreen = ({ navigation }: any) => {
  const [animeList, setAnimeList] = useState<Anime[]>([]);
  const [loading, setLoading] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);

分页列表的标准五件套,之前讲过很多次了。

数据加载

typescript 复制代码
  const loadData = async (pageNum: number, append = false) => {
    try {
      if (pageNum === 1) setLoading(true);
      else setLoadingMore(true);
      
      const res = await getTopAnime(pageNum, 'bypopularity');
      const newData = res.data || [];
      
      if (append) {
        setAnimeList(prev => [...prev, ...newData]);
      } else {
        setAnimeList(newData);
      }
      setHasMore(res.pagination?.has_next_page || false);
    } catch (error) {
      console.error('Load error:', error);
    } finally {
      setLoading(false);
      setLoadingMore(false);
    }
  };

唯一的区别就是 getTopAnime(pageNum, 'bypopularity') 这里的第二个参数。

副作用和回调

typescript 复制代码
  useEffect(() => {
    loadData(1);
  }, []);

  const handleLoadMore = useCallback(() => {
    if (!loadingMore && hasMore) {
      const nextPage = page + 1;
      setPage(nextPage);
      loadData(nextPage, true);
    }
  }, [loadingMore, hasMore, page]);

组件挂载时加载第一页,滚动到底部时加载更多。

渲染列表项

typescript 复制代码
  const renderItem = ({ item, index }: { item: Anime; index: number }) => (
    <AnimeListItem
      anime={item}
      rank={index + 1}
      onPress={() => navigation.navigate('AnimeDetail', { animeId: item.mal_id })}
    />
  );

显示排名,用 index + 1。点击跳转详情页。

条件渲染

typescript 复制代码
  if (loading) {
    return (
      <View style={styles.container}>
        <Header title="人气排行" showBack onBack={() => navigation.goBack()} />
        <Loading fullScreen text="加载中..." />
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Header title="人气排行" showBack onBack={() => navigation.goBack()} />
      <FlatList
        data={animeList}
        renderItem={renderItem}
        keyExtractor={item => item.mal_id.toString()}
        contentContainerStyle={styles.list}
        showsVerticalScrollIndicator={false}
        onEndReached={handleLoadMore}
        onEndReachedThreshold={0.5}
        ListFooterComponent={loadingMore ? <Loading text="加载更多..." /> : null}
        ListEmptyComponent={<EmptyState icon="users" title="暂无数据" />}
      />
    </View>
  );
};

空状态用了 users 图标,暗示"人气"是由用户数量决定的。

样式

typescript 复制代码
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: Colors.background,
  },
  list: {
    padding: Spacing.md,
  },
});

两个样式,简洁明了。

代码复用的思考

看到这里你可能会想:这代码和热门排行、正在热播几乎一模一样啊,就改了个参数和标题,为什么要写成独立的页面?

这是个好问题。有两种处理方式:

方式一:独立页面(当前做法)

每个排行榜一个独立的页面文件。

优点:

  • 代码直观,每个文件职责单一
  • 修改某个页面不会影响其他页面
  • 适合教程项目,读者容易理解

缺点:

  • 代码重复
  • 修改通用逻辑需要改多个文件

方式二:通用页面 + 参数

一个通用的排行榜页面,通过路由参数区分:

typescript 复制代码
// 路由配置
<Stack.Screen name="TopAnime" component={TopAnimeScreen} />

// 导航时传参
navigation.navigate('TopAnime', { 
  filter: 'bypopularity', 
  title: '人气排行' 
});

// 页面内获取参数
const { filter = '', title = '排行榜' } = route.params || {};
const res = await getTopAnime(pageNum, filter);

优点:

  • 代码不重复
  • 修改一处,所有排行榜都生效

缺点:

  • 逻辑稍复杂
  • 如果某个排行榜需要特殊处理,会变得麻烦

实际项目中,方式二更常用。但在教程中,方式一更容易讲解。

人气榜 vs 评分榜的数据差异

拿真实数据举例,人气榜前几名可能是:

  1. Death Note(死亡笔记)
  2. Shingeki no Kyojin(进击的巨人)
  3. Sword Art Online(刀剑神域)

而评分榜前几名可能是:

  1. Fullmetal Alchemist: Brotherhood(钢之炼金术师)
  2. Steins;Gate(命运石之门)
  3. Gintama(银魂)

两个榜单的重合度并不高。人气榜更偏向"大众向"、"话题性强"的作品,评分榜更偏向"口碑好"、"经得起推敲"的作品。

这就是为什么要提供两个榜单:满足不同用户的需求。有人想看"大家都在看什么",有人想看"什么最值得看"。

排名显示的细节

AnimeListItem 组件接收 rank 参数来显示排名:

typescript 复制代码
<AnimeListItem
  anime={item}
  rank={index + 1}
  onPress={...}
/>

组件内部会根据排名显示不同的样式:

  • 前三名可能用金银铜色
  • 其他名次用普通样式

这个逻辑封装在组件内部,页面不需要关心。

但有个潜在问题:当加载第二页时,index 从 0 重新开始。如果第一页有 25 条数据,第二页第一条的 index 是 0,显示的排名是 1,但实际应该是 26。

解决方案是用累计索引:

typescript 复制代码
const renderItem = ({ item, index }: { item: Anime; index: number }) => (
  <AnimeListItem
    anime={item}
    rank={index + 1}  // 这里 FlatList 会自动处理累计索引
    onPress={...}
  />
);

好消息是,FlatList 的 index 是相对于整个 data 数组的,不是相对于当前页的。所以当我们用 [...prev, ...newData] 追加数据时,index 会自动累加。第二页第一条的 index 确实是 25(假设每页 25 条),排名显示 26,是正确的。

小结

人气排行页面展示按人气(用户关注数)排序的动漫,和按评分排序的热门排行形成互补。实现上只是 API 参数不同,其他代码几乎一样。

这种高度相似的页面在实际项目中可以抽象成通用组件,通过参数区分。但在教程项目中,保持独立文件更便于理解和学习。

人气和评分是两个不同的维度。人气高的作品不一定评分高,评分高的作品不一定人气高。提供两个榜单可以满足不同用户的需求。

下一篇是最后一篇,讲最受喜爱页面,展示被收藏最多的动漫。


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

相关推荐
qq_318121592 小时前
互联网大厂Java面试故事:支付与金融服务微服务架构、消息队列与AI风控全流程解析
java·spring boot·redis·微服务·kafka·支付系统·金融服务
短剑重铸之日2 小时前
《7天学会Redis》Day 3 - 持久化机制深度解析
java·redis·后端·缓存
qq_435139572 小时前
多级缓存(Caffeine+Redis)技术实现文档
数据库·redis·缓存
超级种码3 小时前
Redis:Redis持久化机制
数据库·redis·bootstrap
Codeking__4 小时前
Redis初识——Redis的基本特性
数据库·redis·缓存
零度@4 小时前
2026 轻量级消息队列 Redis Stream
前端·redis·bootstrap
難釋懷5 小时前
安装Redis
数据库·redis·缓存
什么都不会的Tristan5 小时前
redis-原理篇-SDS
数据库·redis·缓存
曲幽5 小时前
FastAPI缓存提速实战:手把手教你用Redis为接口注入“记忆”
redis·python·cache·fastapi·web·asyncio