在这里插入图片描述

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

角色列表是动漫详情的延伸页面,展示动漫中的所有角色信息。这篇来讲角色列表页的实现,重点是嵌套数据结构的处理和声优信息的展示。

功能设计

角色列表页需要展示以下信息:

  • 角色头像 - 角色的图片
  • 角色名称 - 角色的名字
  • 角色类型 - Main(主角)或 Supporting(配角)
  • 声优信息 - 配音演员的名字,优先显示日语声优

点击角色卡片可以跳转到角色详情页。

数据结构

角色数据的结构比较复杂,是嵌套的:

interface Character {
  character: {
    mal_id: number;
    name: string;
    images: {
      jpg: {
        image_url: string;
      };
    };
  };
  role: string;
  voice_actors?: VoiceActor[];
}

结构说明:

  • character - 角色基本信息,包含 ID、名字、图片
  • role - 角色类型,Main 或 Supporting
  • voice_actors - 声优数组,可能为空

API 返回的数据结构是这样设计的,角色信息嵌套在 character 字段里。这种设计是因为同一个角色可能出现在多部动漫里,角色本身是独立的实体。

状态定义

const { animeId, title } = route.params;
const [characters, setCharacters] = useState<Character[]>([]);
const [loading, setLoading] = useState(true);

状态说明:

  • animeId - 动漫 ID,用于请求数据
  • title - 动漫标题,显示在页面副标题
  • characters - 角色列表数据
  • loading - 加载状态

把 title 也传过来是为了在 Header 里显示,让用户知道这是哪部动漫的角色列表。

数据加载

useEffect(() => {
  loadCharacters();
}, []);

const loadCharacters = async () => {
  try {
    const data = await getAnimeCharacters(animeId);
    setCharacters(data.data || []);
  } catch (error) {
    console.error('Load characters error:', error);
  } finally {
    setLoading(false);
  }
};

加载逻辑:

  • 组件挂载时加载数据
  • data.data || [] 处理空数据
  • 错误时打印日志,不影响页面显示

这个页面不需要分页,因为一部动漫的角色数量通常不会太多,一次性加载即可。

角色卡片渲染

const renderItem = ({ item }: { item: Character }) => (
  <TouchableOpacity 
    style={styles.characterCard}
    onPress={() => navigation.navigate('CharacterDetail', { 
      characterId: item.character.mal_id 
    })}
  >

卡片入口:

  • 整个卡片可点击
  • 点击跳转到角色详情页
  • 传递 characterId 参数

注意这里用的是 item.character.mal_id,因为数据是嵌套的。

    <Image
      source={{ uri: item.character.images?.jpg?.image_url }}
      style={styles.characterImage}
    />

角色头像:

  • 从嵌套结构中取图片 URL
  • 用可选链 ?. 防止空值报错
    <View style={styles.characterInfo}>
      <Text style={styles.characterName}>{item.character.name}</Text>
      <Text style={styles.characterRole}>{item.role}</Text>

角色信息:

  • 名字用主要文字颜色
  • 角色类型用主题色,区分主角和配角

角色类型用主题色高亮,让用户一眼就能看出哪些是主角。

声优信息处理

      {item.voice_actors && item.voice_actors.length > 0 && (
        <View style={styles.voiceActor}>
          <Text style={styles.vaLabel}>CV:</Text>
          <Text style={styles.vaName}>
            {item.voice_actors.find(va => va.language === 'Japanese')?.person.name || 
             item.voice_actors[0]?.person.name}
          </Text>
        </View>
      )}
    </View>
  </TouchableOpacity>
);

声优显示逻辑:

  • 先检查 voice_actors 是否存在且不为空
  • 优先显示日语声优
  • 如果没有日语声优,显示第一个声优

动漫通常有多国配音,但用户最关心的是日语原版声优。用 find 方法查找 language 为 Japanese 的声优,找不到就用第一个。

{item.voice_actors.find(va => va.language === 'Japanese')?.person.name || 
 item.voice_actors[0]?.person.name}

这行代码的逻辑:

  • find(va => va.language === 'Japanese') - 查找日语声优
  • ?.person.name - 如果找到了,取声优名字
  • || - 如果没找到日语声优
  • item.voice_actors[0]?.person.name - 取第一个声优的名字

这是一个常见的"优先取 A,否则取 B"的模式。用可选链和逻辑或组合实现。

页面结构

return (
  <View style={styles.container}>
    <Header
      title="角色"
      subtitle={title}
      showBack
      onBack={() => navigation.goBack()}
    />

Header 组件:

  • title 显示"角色"
  • subtitle 显示动漫标题
  • showBack 显示返回按钮
  • onBack 处理返回逻辑

Header 是封装好的通用组件,在多个页面复用。通过 Props 控制显示内容和行为。

条件渲染

    {loading ? (
      <Loading fullScreen />
    ) : characters.length === 0 ? (
      <EmptyState icon="people" title="暂无角色信息" />
    ) : (
      <FlatList
        data={characters}
        renderItem={renderItem}
        keyExtractor={(item) => item.character.mal_id.toString()}
        contentContainerStyle={styles.list}
        showsVerticalScrollIndicator={false}
      />
    )}
  </View>
);

三种状态:

  • 加载中 - 显示 Loading
  • 无数据 - 显示空状态,用人物图标
  • 有数据 - 显示列表

空状态用 people 图标,和角色主题呼应。

FlatList 配置

<FlatList
  data={characters}
  renderItem={renderItem}
  keyExtractor={(item) => item.character.mal_id.toString()}
  contentContainerStyle={styles.list}
  showsVerticalScrollIndicator={false}
/>

配置说明:

  • keyExtractor - 用角色 ID 作为 key
  • contentContainerStyle - 设置内容区域的样式
  • 隐藏滚动条

注意 keyExtractor 里用的是 item.character.mal_id,因为数据是嵌套的。

卡片样式

characterCard: {
  flexDirection: 'row',
  backgroundColor: Colors.backgroundCard,
  borderRadius: BorderRadius.lg,
  marginBottom: Spacing.md,
  overflow: 'hidden',
},

卡片容器:

  • 横向布局,图片在左,信息在右
  • 卡片背景色,圆角
  • overflow: 'hidden' 让图片的圆角生效

overflow: 'hidden' 很重要,如果不设置,图片会超出圆角边界。

characterImage: {
  width: 80,
  height: 100,
  backgroundColor: Colors.backgroundLight,
},

图片样式:

  • 固定宽高 80x100
  • 设置背景色,图片加载前显示

图片比例大约是 4:5,这是人物头像的常见比例。

characterInfo: {
  flex: 1,
  padding: Spacing.md,
  justifyContent: 'center',
},

信息区域:

  • flex: 1 占据剩余空间
  • 内边距让内容不贴边
  • 垂直居中
characterName: {
  fontSize: FontSize.md,
  fontWeight: '600',
  color: Colors.text,
},
characterRole: {
  fontSize: FontSize.sm,
  color: Colors.primary,
  marginTop: Spacing.xs,
},

名字和角色类型:

  • 名字用正常字号,加粗
  • 角色类型用小字号,主题色
voiceActor: {
  flexDirection: 'row',
  marginTop: Spacing.sm,
},
vaLabel: {
  fontSize: FontSize.sm,
  color: Colors.textMuted,
},
vaName: {
  fontSize: FontSize.sm,
  color: Colors.textSecondary,
  marginLeft: Spacing.xs,
},

声优信息样式:

  • 横向布局,CV 标签和名字在一行
  • 标签用最淡的颜色
  • 名字用次要颜色

CV 是 Character Voice 的缩写,在动漫圈是常用的术语。

列表容器样式

list: {
  padding: Spacing.lg,
},

内边距:

只设置了 padding,没有设置 paddingBottom。因为这个页面没有 Tab 栏,不需要额外的底部留白。

与详情页的关系

角色列表页是从详情页跳转过来的:

// 详情页的跳转代码
navigation.navigate('AnimeCharacters', { 
  animeId: anime.mal_id, 
  title: anime.title 
})

参数传递:

  • animeId 用于请求角色数据
  • title 用于显示在 Header 副标题

这种设计让用户知道当前在看哪部动漫的角色,不会迷失方向。

跳转到角色详情

点击角色卡片会跳转到角色详情页:

navigation.navigate('CharacterDetail', { 
  characterId: item.character.mal_id 
})

只传 characterId:

角色详情页会根据 characterId 请求完整的角色信息,不需要传其他参数。

数据流向

整个数据流向是这样的:

  • 首页/搜索页 → 动漫详情页(传 animeId)
  • 动漫详情页 → 角色列表页(传 animeId, title)
  • 角色列表页 → 角色详情页(传 characterId)

每一层只传必要的参数,下一层自己请求需要的数据。这样可以保证数据的新鲜度,也避免传递大量数据。

性能考虑

角色列表通常不会太长,一部动漫的角色数量一般在几十个以内。所以:

  • 不需要分页加载
  • 不需要虚拟列表优化
  • 一次性加载所有数据即可

如果遇到角色特别多的动漫(比如群像剧),可能需要考虑分页。但目前的实现对大多数情况够用了。

小结

角色列表页的核心是处理嵌套的数据结构和声优信息的优先级显示。数据结构嵌套时要注意用可选链防止空值报错,声优信息用 find + 逻辑或实现优先级。

卡片布局用横向排列,图片在左信息在右,是列表项的常见设计。角色类型用主题色高亮,让用户快速区分主角和配角。

下一篇会讲角色详情页,展示单个角色的完整信息。


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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐