在这里插入图片描述

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

玩一个新英雄,除了看技能说明,最想知道的就是"这个英雄怎么玩"和"怎么打这个英雄"。拳头官方为每个英雄都准备了使用技巧(allytips)和对战建议(enemytips),我们把这些信息整理成一个清晰的列表页面。

这篇文章的重点是列表渲染、空状态处理、以及如何用颜色区分不同类型的内容。

两种技巧的本质区别

API 返回的英雄数据中有两个数组字段,它们面向的用户群体完全不同:

allytips(使用技巧) 是给想玩这个英雄的人看的。比如盖伦的使用技巧可能会告诉你"E 技能可以在草丛中提前开启,出草丛时伤害已经叠满",这种信息能帮助新手更快上手一个英雄。

enemytips(对战建议) 是给要对抗这个英雄的人看的。比如"盖伦的被动在脱战后会快速回血,要持续消耗他不让他回血",知道这个信息后,你在对线时就会有意识地保持骚扰。

我们用颜色来区分这两类信息——使用技巧用绿色(代表"对你有利"),对战建议用红色(代表"需要警惕")。这种颜色语义在很多应用中都是通用的,用户不需要学习就能理解。

页面整体结构

import React, {useEffect, useState} from 'react';
import {View, Text, ScrollView, StyleSheet} from 'react-native';
import {colors} from '../../styles/colors';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {championApi} from '../../api';
import {Loading} from '../../components/common';
import type {ChampionDetail} from '../../models/Champion';

这个页面不需要图片相关的组件,是一个纯文本展示页面。ScrollView 用来处理内容超出一屏的情况,因为有些英雄的技巧条目比较多。

export function ChampionTipsPage() {
  const {state} = useApp();
  const {params} = useNavigation();
  const [champion, setChampion] = useState<ChampionDetail | null>(null);
  const [loading, setLoading] = useState(true);

  const championId = params.championId as string;

状态管理延续了之前页面的模式。champion 存储英雄详情数据,loading 控制加载状态的显示。championId 从路由参数获取,这个参数是从英雄详情页跳转过来时传入的。

数据加载的实现

  useEffect(() => {
    async function loadDetail() {
      if (!state.version || !championId) return;

      setLoading(true);
      const detail = await championApi.getChampionDetail(
        state.version,
        championId,
      );
      setChampion(detail);
      setLoading(false);
    }

    loadDetail();
  }, [state.version, championId]);

  if (loading || !champion) {
    return <Loading fullScreen />;
  }

这段数据加载的代码在前面几篇文章中出现过很多次了。你可能会想,能不能把它抽取成一个自定义 Hook?当然可以:

// hooks/useChampionDetail.ts
export function useChampionDetail(championId: string) {
  const {state} = useApp();
  const [champion, setChampion] = useState<ChampionDetail | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 加载逻辑...
  }, [state.version, championId]);

  return {champion, loading};
}

抽取后,页面组件就可以简化成:

const {champion, loading} = useChampionDetail(championId);

不过为了让每篇文章的代码相对独立、便于理解,我们在这个系列中保持了完整的写法。实际项目中推荐做这种抽取,能减少重复代码。

使用技巧列表的渲染

  return (
    <ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
      {/* 使用技巧 */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>💡 使用技巧</Text>
        {champion.allytips.length > 0 ? (
          champion.allytips.map((tip, index) => (
            <View key={index} style={styles.tipCard}>
              <View style={styles.tipNumber}>
                <Text style={styles.tipNumberText}>{index + 1}</Text>
              </View>
              <Text style={styles.tipText}>{tip}</Text>
            </View>
          ))
        ) : (
          <View style={styles.emptyCard}>
            <Text style={styles.emptyText}>暂无使用技巧</Text>
          </View>
        )}
      </View>

这段代码有几个值得展开说的点。

序号设计的考量

每条技巧前面有一个圆形的序号标记,用 index + 1 生成(因为 index 从 0 开始,显示给用户要从 1 开始)。

为什么要加序号?有几个原因:

  1. 技巧之间可能有先后顺序或重要程度的区别
  2. 序号能帮助用户记忆和引用,比如和朋友讨论时可以说"第 2 条技巧说的那个"
  3. 视觉上更有层次感,不会显得是一堆无序的文字

空状态的处理

{champion.allytips.length > 0 ? (
  // 渲染列表
) : (
  <View style={styles.emptyCard}>
    <Text style={styles.emptyText}>暂无使用技巧</Text>
  </View>
)}

有些英雄的 allytips 数组是空的,这时候显示"暂无使用技巧"比什么都不显示要好。用户至少知道这里应该有内容,只是暂时没有,而不是以为页面加载出了问题或者漏掉了什么。

空状态处理是一个容易被忽视但很重要的细节。好的空状态设计应该:

  • 明确告诉用户"这里没有内容"
  • 如果可能,解释为什么没有内容
  • 如果可能,提供下一步操作的建议

在这个场景下,我们只做了第一点,因为内容是拳头官方提供的,我们无法控制也无法引导用户去创建内容。

key 的选择

{champion.allytips.map((tip, index) => (
  <View key={index} style={styles.tipCard}>

这里用 index 作为 key。通常我们不推荐用 index 做 key,因为如果列表会重新排序或者动态增删,React 可能会复用错误的组件实例。但在这个场景下,技巧列表是静态的,不会变化,用 index 是安全的。

如果你想更严谨,可以用技巧内容的哈希值作为 key:

key={`tip-${tip.substring(0, 20)}-${index}`}

对战建议列表的渲染

      {/* 对战建议 */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>⚔️ 对战建议</Text>
        {champion.enemytips.length > 0 ? (
          champion.enemytips.map((tip, index) => (
            <View key={index} style={[styles.tipCard, styles.enemyTipCard]}>
              <View style={[styles.tipNumber, styles.enemyTipNumber]}>
                <Text style={styles.tipNumberText}>{index + 1}</Text>
              </View>
              <Text style={styles.tipText}>{tip}</Text>
            </View>
          ))
        ) : (
          <View style={styles.emptyCard}>
            <Text style={styles.emptyText}>暂无对战建议</Text>
          </View>
        )}
      </View>

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

对战建议的结构和使用技巧完全一样,区别只在样式上。看这行代码:

style={[styles.tipCard, styles.enemyTipCard]}

React Native 的 style 属性支持数组,数组中的样式会按顺序合并,后面的会覆盖前面的同名属性。这种"基础样式 + 变体样式"的模式非常实用:

  • tipCard 定义了卡片的通用样式:背景色、圆角、内边距、边框等
  • enemyTipCard 只覆盖需要变化的部分:左边框颜色从绿色变成红色

这样做的好处是代码更 DRY(Don’t Repeat Yourself)。如果以后要改卡片的圆角大小,只需要改 tipCard 一处,两种卡片都会生效。

样式设计详解

容器和区块样式

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: colors.background,
    padding: 16,
  },
  section: {
    marginBottom: 24,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: colors.textPrimary,
    marginBottom: 12,
  },

section 之间留 24px 的间距,让"使用技巧"和"对战建议"两个列表在视觉上明显分开。如果间距太小,用户可能会把两个列表混为一谈。

技巧卡片样式

  tipCard: {
    flexDirection: 'row',
    backgroundColor: colors.backgroundCard,
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
    borderWidth: 1,
    borderColor: colors.border,
    borderLeftWidth: 3,
    borderLeftColor: colors.success,
  },
  enemyTipCard: {
    borderLeftColor: colors.error,
  },

卡片左边有一条 3px 的彩色边框,这是一个常见的设计手法,叫做"色带指示器"(color bar indicator)。它能在不占用太多空间的情况下传达信息类型:

  • 绿色(success)= 对你有利的信息 = 使用技巧
  • 红色(error)= 需要警惕的信息 = 对战建议

用户扫一眼颜色就知道这是哪类信息,不需要每次都读标题。

flexDirection: 'row' 让序号和文字横向排列。默认的 column 布局会让它们上下排列,不符合我们的设计。

序号样式

  tipNumber: {
    width: 24,
    height: 24,
    borderRadius: 12,
    backgroundColor: colors.success,
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 12,
  },
  enemyTipNumber: {
    backgroundColor: colors.error,
  },
  tipNumberText: {
    fontSize: 12,
    fontWeight: 'bold',
    color: colors.white,
  },

序号用一个圆形背景包裹。实现圆形的方法是:width 和 height 相等,borderRadius 是它们的一半。24x24 的圆形配合 12px 的字号,数字在视觉上居中且不会显得拥挤。

alignItems: 'center'justifyContent: 'center' 配合使用,让数字在圆形中水平和垂直都居中。这是 Flexbox 居中的标准写法。

文字样式

  tipText: {
    flex: 1,
    fontSize: 14,
    color: colors.textSecondary,
    lineHeight: 20,
  },

flex: 1 让文字区域占据序号之外的所有剩余宽度。如果不设置这个,文字区域的宽度会由内容决定,可能会导致布局问题。

lineHeight: 20 配合 fontSize: 14,行高是字号的 1.43 倍。这个比例让多行文字阅读起来比较舒适,行与行之间不会太挤也不会太松。

空状态样式

  emptyCard: {
    backgroundColor: colors.backgroundCard,
    borderRadius: 8,
    padding: 24,
    alignItems: 'center',
    borderWidth: 1,
    borderColor: colors.border,
  },
  emptyText: {
    fontSize: 14,
    color: colors.textMuted,
  },
  bottomSpace: {
    height: 20,
  },
});

空状态卡片的 padding 比普通卡片大(24 vs 12),让"暂无内容"的提示不会显得太局促,有一种"留白"的感觉。

文字用 textMuted 颜色,比 textSecondary 更淡。这是一个视觉暗示:这条信息不重要,只是一个占位说明。

数据结构补充说明

allytips 和 enemytips 都是字符串数组,每个元素是一条独立的技巧:

interface ChampionDetail {
  // ... 其他字段
  allytips: string[];   // ["技巧1", "技巧2", "技巧3"]
  enemytips: string[];  // ["建议1", "建议2"]
}

数组长度不固定,有的英雄有 3 条,有的有 5 条,有的一条都没有。拳头的数据质量参差不齐,有些英雄的技巧写得很详细很实用,有些就比较敷衍甚至过时了。这不是我们能控制的,只能如实展示。

如果你想提供更好的内容,可以考虑自建数据库,收集社区的高质量攻略。但这超出了这个项目的范围。

可能的优化方向

当前实现已经能满足基本需求,但如果想做得更好,还有一些可以改进的地方。

技巧分类:有些技巧是关于对线的,有些是关于团战的,有些是关于出装的。如果能自动分类或者让用户筛选,体验会更好。不过这需要对技巧内容做自然语言处理,实现复杂度较高。

用户贡献:官方的技巧有限且可能过时,可以让用户提交自己的心得,形成一个 UGC(用户生成内容)社区。这需要后端支持用户系统、内容审核等,是一个完整的产品方向。

关联展示:技巧中提到的技能名称可以做成可点击的链接,跳转到对应的技能详情。比如"盖伦的 E 技能"可以点击跳转到 E 技能的详细说明。这需要解析文本中的技能名称并做匹配,有一定的技术挑战。

收藏功能:让用户可以收藏觉得有用的技巧,方便以后查看。这需要本地存储或者用户账号系统的支持。

小结

使用技巧页是一个典型的列表展示页面,虽然功能简单,但涉及到几个重要的前端开发概念:

  1. 条件渲染:根据数据是否为空决定渲染列表还是空状态
  2. 样式复用:用"基础样式 + 变体样式"的模式减少重复代码
  3. 颜色语义:用颜色传达信息类型,减少用户的认知负担
  4. 空状态设计:妥善处理没有数据的情况,给用户明确的反馈

下一篇我们来实现英雄筛选功能,让用户可以按职业、难度等条件筛选英雄列表,这会涉及到更复杂的状态管理和列表过滤逻辑。


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

Logo

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

更多推荐