在这里插入图片描述

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

英雄联盟有 200 多件装备,从基础的长剑、布甲,到成型的无尽之刃、兰顿之兆。装备大全页要把这些装备分类展示,并支持搜索和筛选,让用户能快速找到想要的装备。

这篇文章我们来实现装备列表页,重点是分类标签栏的设计、搜索和筛选的组合使用、以及数据缓存策略。

装备数据的特点

和英雄数据不同,装备数据有一些特殊之处:

  1. 数量更多:200+ 件装备,比英雄还多
  2. 分类复杂:装备有多种分类方式,按属性(攻击、防御、法术等)、按类型(基础装备、成型装备)
  3. 合成关系:装备之间有合成关系,A + B = C
  4. 价格属性:每件装备都有价格,影响出装顺序

我们先实现基础的列表展示和分类筛选,合成关系在后面的文章中单独讲。

页面结构

import React, {useEffect, useState, useMemo} from 'react';
import {View, StyleSheet} from 'react-native';
import {useTheme} from '../../context/ThemeContext';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {itemApi} from '../../api';
import {ItemGrid} from '../../components/item';
import {SearchBar, TabBar, Loading} from '../../components/common';
import {useDebounce} from '../../hooks';
import type {Item} from '../../models/Item';

这个页面用到了几个新组件:

  • ItemGrid:装备网格组件,类似 ChampionGrid
  • TabBar:分类标签栏,支持横向滚动
export function ItemListPage() {
  const {colors} = useTheme();
  const {state, setItems} = useApp();
  const {navigate} = useNavigation();
  const [loading, setLoading] = useState(true);
  const [searchText, setSearchText] = useState('');
  const [activeTag, setActiveTag] = useState('All');

  const debouncedSearch = useDebounce(searchText, 300);
  const categories = itemApi.getItemCategories();

状态比英雄列表页多了一个 activeTag,用于记录当前选中的分类。categories 是装备分类列表,从 API 模块获取。

数据加载与缓存

  useEffect(() => {
    async function loadItems() {
      if (!state.version) return;
      if (state.items.length === 0) {
        setLoading(true);
        const items = await itemApi.getItemList(state.version);
        setItems(items);
      }
      setLoading(false);
    }
    loadItems();
  }, [state.version]);

这里有一个缓存策略:if (state.items.length === 0) 判断全局状态中是否已经有装备数据。如果有,就不再请求,直接使用缓存的数据。

为什么要这样做?因为装备数据量大(200+ 条),每次进入页面都请求会浪费流量和时间。装备数据在一个游戏版本内不会变化,缓存是安全的。

数据存储在 AppContext 的全局状态中,通过 setItems 方法更新。这样其他页面(比如装备详情页)也可以直接使用这些数据,不需要重复请求。

筛选逻辑

  const filteredItems = useMemo(() => {
    let result = state.items;
    if (activeTag !== 'All') {
      result = itemApi.filterItemsByTag(result, activeTag);
    }
    if (debouncedSearch) {
      result = itemApi.searchItems(result, debouncedSearch);
    }
    return result;
  }, [state.items, activeTag, debouncedSearch]);

筛选逻辑和英雄筛选类似,但这里是分类筛选和搜索的组合。两个条件是"与"的关系:先按分类筛选,再在结果中搜索。

比如用户选了"攻击"分类,然后搜索"剑",会显示所有攻击类装备中名称包含"剑"的装备。

itemApi.filterItemsByTagitemApi.searchItems 是封装的工具函数:

// api/item.ts
export function filterItemsByTag(items: Item[], tag: string): Item[] {
  return items.filter(item => item.tags.includes(tag));
}

export function searchItems(items: Item[], keyword: string): Item[] {
  const lower = keyword.toLowerCase();
  return items.filter(item => 
    item.name.toLowerCase().includes(lower) ||
    item.description.toLowerCase().includes(lower)
  );
}

分类标签栏

装备分类比较多,一行放不下,需要支持横向滚动。

  const categories = itemApi.getItemCategories();
  // 返回类似这样的数组:
  // [
  //   {key: 'All', label: '全部'},
  //   {key: 'Damage', label: '攻击'},
  //   {key: 'Defense', label: '防御'},
  //   {key: 'SpellDamage', label: '法术'},
  //   {key: 'AttackSpeed', label: '攻速'},
  //   {key: 'LifeSteal', label: '吸血'},
  //   ...
  // ]

TabBar 组件接收这个数组,渲染成可滚动的标签栏:

<TabBar 
  tabs={categories} 
  activeKey={activeTag} 
  onTabChange={setActiveTag} 
  scrollable 
/>

scrollable 属性启用横向滚动。当标签总宽度超过屏幕宽度时,用户可以左右滑动查看更多标签。

动态样式

  const styles = useMemo(() => StyleSheet.create({
    container: {flex: 1, backgroundColor: colors.background},
    header: {marginBottom: 8},
    tabContainer: {marginTop: 12},
  }), [colors]);

样式用 useMemo 包裹,依赖 colors。当主题切换时,colors 变化,样式会重新生成。这是支持深色/浅色模式切换的关键。

为什么不直接写成静态样式?因为 colors.background 是动态的,在深色模式下是深色,浅色模式下是浅色。如果写成静态样式,颜色就固定了,无法响应主题切换。

页面渲染

  const handleItemPress = (item: Item) => {
    navigate('ItemDetail', {itemId: item.id});
  };

  if (loading) return <Loading fullScreen />;

  const ListHeader = (
    <View style={styles.header}>
      <SearchBar 
        value={searchText} 
        onChangeText={setSearchText} 
        placeholder="搜索装备名称..." 
      />
      <View style={styles.tabContainer}>
        <TabBar 
          tabs={categories} 
          activeKey={activeTag} 
          onTabChange={setActiveTag} 
          scrollable 
        />
      </View>
    </View>
  );

  return (
    <View style={styles.container}>
      <ItemGrid 
        items={filteredItems} 
        version={state.version} 
        onItemPress={handleItemPress} 
        ListHeaderComponent={ListHeader} 
      />
    </View>
  );
}

ListHeader 包含搜索框和分类标签栏,放在 ItemGrid 的顶部。搜索框在上,标签栏在下,这是常见的布局方式。

TabBar 组件的实现

TabBar 是一个通用的标签栏组件,支持固定宽度和可滚动两种模式。

interface TabBarProps {
  tabs: Array<{key: string; label: string}>;
  activeKey: string;
  onTabChange: (key: string) => void;
  scrollable?: boolean;
}

export function TabBar({tabs, activeKey, onTabChange, scrollable}: TabBarProps) {
  const Container = scrollable ? ScrollView : View;
  const containerProps = scrollable ? {
    horizontal: true,
    showsHorizontalScrollIndicator: false,
  } : {};

  return (
    <Container style={styles.container} {...containerProps}>
      {tabs.map(tab => (
        <TouchableOpacity
          key={tab.key}
          style={[styles.tab, activeKey === tab.key && styles.tabActive]}
          onPress={() => onTabChange(tab.key)}>
          <Text style={[styles.tabText, activeKey === tab.key && styles.tabTextActive]}>
            {tab.label}
          </Text>
        </TouchableOpacity>
      ))}
    </Container>
  );
}

scrollable 属性决定使用 ScrollView 还是普通 View 作为容器。ScrollView 设置 horizontal: true 启用横向滚动。

这种设计让组件更灵活:标签少的时候用固定布局,标签多的时候用滚动布局,同一个组件都能处理。

装备数据结构

装备的数据结构比英雄简单一些:

interface Item {
  id: string;
  name: string;
  description: string;
  plaintext: string;  // 简短描述
  gold: {
    base: number;     // 基础价格
    total: number;    // 总价格
    sell: number;     // 出售价格
  };
  tags: string[];     // 分类标签
  stats: Record<string, number>;  // 属性加成
  from?: string[];    // 合成所需的装备 ID
  into?: string[];    // 可以合成的装备 ID
  image: {
    full: string;
  };
}

frominto 字段描述了装备的合成关系,我们会在"合成树"那篇文章中详细讲解。

ItemGrid 组件

ItemGrid 和 ChampionGrid 类似,用 FlatList 实现网格布局:

export function ItemGrid({items, version, onItemPress, ListHeaderComponent}) {
  return (
    <FlatList
      data={items}
      keyExtractor={item => item.id}
      numColumns={4}
      contentContainerStyle={styles.list}
      ListHeaderComponent={ListHeaderComponent}
      renderItem={({item}) => (
        <ItemCard 
          item={item} 
          version={version} 
          onPress={() => onItemPress(item)} 
        />
      )}
    />
  );
}

装备图标比英雄头像小,所以用 4 列布局(英雄用 3 列)。这样一屏能显示更多装备,方便用户浏览。

性能优化

装备列表有 200+ 条数据,需要注意性能:

FlatList 的虚拟化:FlatList 默认只渲染可视区域的元素,滚动时动态加载。这是处理长列表的标准方案。

图片缓存:装备图标会被 React Native 自动缓存,同一张图片只下载一次。

useMemo 优化:筛选逻辑用 useMemo 包裹,只有依赖项变化时才重新计算。

防抖搜索:搜索输入用 useDebounce 防抖,避免频繁触发筛选。

可能的优化方向

价格排序:加一个排序选项,让用户可以按价格从低到高或从高到低排序。对于想看便宜装备或贵装备的用户很有用。

属性筛选:除了分类筛选,还可以按具体属性筛选,比如"加攻击力的装备"、“加暴击的装备”。

收藏功能:让用户可以收藏常用的装备,方便快速查看。

出装推荐:根据用户选择的英雄,推荐适合的装备。这需要额外的数据支持。

小结

装备大全页在英雄列表页的基础上增加了几个功能:

  1. 分类标签栏:用 TabBar 组件实现可滚动的分类筛选
  2. 搜索 + 筛选组合:两个条件同时生效,缩小结果范围
  3. 数据缓存:装备数据存储在全局状态中,避免重复请求
  4. 动态样式:用 useMemo 生成样式,支持主题切换

下一篇我们来实现装备详情页,展示装备的属性、价格和合成信息。


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

Logo

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

更多推荐