在这里插入图片描述

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

一件成型装备可能需要好几层合成。比如无尽之刃需要暴风大剑 + 斗篷 + 长剑,而暴风大剑又需要长剑 + 长剑。合成树功能把这种层级关系用树形结构展示出来,让玩家一眼就能看清完整的合成路径。

这篇文章我们来实现合成树页面,重点是递归数据结构的构建、树形 UI 的渲染、以及缩进层级的处理。

合成关系的数据结构

在装备数据中,每件装备都有一个 from 字段,记录合成它需要哪些装备的 ID:

// 无尽之刃的数据
{
  id: "3031",
  name: "无尽之刃",
  from: ["1038", "1018", "1036"],  // 暴风大剑、斗篷、长剑
  // ...
}

// 暴风大剑的数据
{
  id: "1038",
  name: "暴风大剑",
  from: ["1036", "1036"],  // 两把长剑
  // ...
}

// 长剑的数据(基础装备,没有 from)
{
  id: "1036",
  name: "长剑",
  // 没有 from 字段,说明是基础装备
}

我们要做的就是根据这个 from 关系,递归地构建出完整的合成树。

页面结构

import React from 'react';
import {View, Text, ScrollView, Image, StyleSheet} from 'react-native';
import {colors} from '../../styles/colors';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {getItemIconUrl} from '../../utils/image';
import {formatNumber} from '../../utils/format';

export function ItemBuildPage() {
  const {state} = useApp();
  const {params} = useNavigation();

  const itemId = params.itemId as string;
  const item = state.items.find(i => i.id === itemId);

  if (!item) {
    return (
      <View style={styles.container}>
        <Text style={styles.errorText}>装备不存在</Text>
      </View>
    );
  }

和装备详情页一样,从全局状态中查找装备数据。如果找不到,显示错误提示。

递归构建合成树

这是整个页面的核心逻辑。我们用递归函数遍历合成关系,构建出树形数据结构。

  // 递归获取合成树
  const getBuildTree = (id: string, depth = 0): any => {
    const currentItem = state.items.find(i => i.id === id);
    if (!currentItem) return null;

    return {
      item: currentItem,
      depth,
      children: currentItem.from?.map(fromId => getBuildTree(fromId, depth + 1)) || [],
    };
  };

  const buildTree = getBuildTree(itemId);

这个函数做了什么:

  1. 根据 ID 查找装备数据
  2. 如果找不到,返回 null(递归终止条件之一)
  3. 如果找到了,返回一个节点对象,包含:
    • item:装备数据
    • depth:当前深度(用于计算缩进)
    • children:子节点数组,通过递归调用生成

递归的终止条件有两个:

  • 找不到装备(返回 null)
  • 装备没有 from 字段(基础装备,children 为空数组)

以无尽之刃为例,生成的树结构大概是这样的:

{
  item: { name: "无尽之刃", ... },
  depth: 0,
  children: [
    {
      item: { name: "暴风大剑", ... },
      depth: 1,
      children: [
        { item: { name: "长剑", ... }, depth: 2, children: [] },
        { item: { name: "长剑", ... }, depth: 2, children: [] },
      ]
    },
    { item: { name: "斗篷", ... }, depth: 1, children: [] },
    { item: { name: "长剑", ... }, depth: 1, children: [] },
  ]
}

递归渲染树节点

有了树形数据,接下来用递归函数渲染 UI。

  const renderTreeNode = (node: any) => {
    if (!node) return null;

    return (
      <View key={node.item.id} style={styles.treeNode}>
        <View style={[styles.itemRow, {marginLeft: node.depth * 20}]}>
          <Image
            source={{uri: getItemIconUrl(state.version, node.item.id)}}
            style={styles.itemIcon}
          />
          <View style={styles.itemInfo}>
            <Text style={styles.itemName}>{node.item.name}</Text>
            <Text style={styles.itemGold}>
              💰 {formatNumber(node.item.gold?.total || 0)}
            </Text>
          </View>
        </View>
        {node.children.length > 0 && (
          <View style={styles.childrenContainer}>
            {node.children.map((child: any) => renderTreeNode(child))}
          </View>
        )}
      </View>
    );
  };

关键点解析:

缩进的实现marginLeft: node.depth * 20 根据深度计算左边距。深度 0 没有缩进,深度 1 缩进 20px,深度 2 缩进 40px,以此类推。

递归渲染子节点:如果有子节点,遍历 children 数组,对每个子节点递归调用 renderTreeNode。

key 的选择:用 node.item.id 作为 key。注意同一件装备可能在树中出现多次(比如长剑),但 React 的 key 只需要在同级元素中唯一,不同层级可以重复。

页面渲染

  return (
    <ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
      <View style={styles.header}>
        <Text style={styles.title}>合成树</Text>
        <Text style={styles.subtitle}>{item.name}</Text>
      </View>

      <View style={styles.treeContainer}>
        {renderTreeNode(buildTree)}
      </View>

      {/* 总价计算 */}
      <View style={styles.totalCard}>
        <Text style={styles.totalLabel}>总价</Text>
        <Text style={styles.totalValue}>
          💰 {formatNumber(item.gold?.total || 0)}
        </Text>
      </View>

      <View style={styles.bottomSpace} />
    </ScrollView>
  );
}

页面分三部分:

  • 头部:显示"合成树"标题和装备名称
  • 树形区域:展示完整的合成路径
  • 总价卡片:显示合成这件装备需要的总金币

样式设计

树节点样式

  treeNode: {
    marginBottom: 8,
  },
  itemRow: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 8,
    borderLeftWidth: 2,
    borderLeftColor: colors.primary,
    paddingLeft: 12,
  },
  itemIcon: {
    width: 36,
    height: 36,
    borderRadius: 4,
    borderWidth: 1,
    borderColor: colors.border,
  },
  itemInfo: {
    marginLeft: 12,
    flex: 1,
  },

每个节点左边有一条金色的竖线(borderLeftWidth: 2),形成视觉上的连接感。图标尺寸 36x36,比详情页的小一些,因为树中可能有很多节点。

子节点容器样式

  childrenContainer: {
    marginLeft: 16,
    borderLeftWidth: 1,
    borderLeftColor: colors.border,
  },

子节点容器也有一条竖线,但颜色是灰色的,和父节点的金色竖线区分开。这样形成了一个层级分明的视觉效果。

总价卡片样式

  totalCard: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    backgroundColor: colors.backgroundCard,
    borderRadius: 8,
    padding: 16,
    marginTop: 16,
    borderWidth: 1,
    borderColor: colors.borderGold,
  },
  totalValue: {
    fontSize: 20,
    fontWeight: 'bold',
    color: colors.textGold,
  },

总价卡片用金色边框突出显示,价格用大号金色字体,让用户一眼就能看到。

性能考虑

递归在数据量大的时候可能有性能问题。不过英雄联盟的装备合成树通常不会太深(最多 3-4 层),节点数量也不会太多(最多十几个),所以不需要特别优化。

如果你的场景有更深的树结构,可以考虑:

虚拟化渲染:只渲染可视区域的节点,滚动时动态加载。不过 React Native 的 FlatList 不太适合树形结构,需要自己实现。

折叠功能:默认只展开第一层,用户点击后展开更深的层级。这样初始渲染的节点数量会少很多。

缓存计算结果:用 useMemo 缓存 buildTree 的结果,避免每次渲染都重新计算。

const buildTree = useMemo(() => getBuildTree(itemId), [itemId, state.items]);

交互优化

当前实现是纯展示的,可以加一些交互功能:

点击跳转:点击树中的任意装备,跳转到那件装备的详情页。

<TouchableOpacity onPress={() => navigate('ItemDetail', {itemId: node.item.id})}>
  {/* 节点内容 */}
</TouchableOpacity>

展开/折叠:让用户可以折叠不关心的分支,专注于某一条合成路径。

高亮路径:当用户点击某个节点时,高亮从根节点到该节点的路径。

小结

合成树页面的核心是递归——递归构建数据结构,递归渲染 UI。这是处理树形数据的标准方法。

关键技术点:

  1. 递归函数:用递归遍历合成关系,构建树形数据
  2. 深度参数:记录每个节点的深度,用于计算缩进
  3. 递归渲染:用递归函数渲染树节点,处理任意深度的树
  4. 视觉层级:用缩进和竖线表示层级关系

下一篇我们来实现装备筛选功能,让用户可以按分类和属性筛选装备。


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

Logo

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

更多推荐