React Native for OpenHarmony 实战:Steam 资讯 App 即将推出页面

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

作为一个老玩家,我经常会关注 Steam 上即将发售的游戏。有时候看到感兴趣的新作,提前加个愿望单,等发售了第一时间入手。这篇文章来实现"即将推出"页面,也是 featuredcategories 接口系列的收官之作。
请添加图片描述

即将推出 vs 新品上架

这两个页面容易混淆,先理清一下:

  • 新品上架(new_releases):已经发售的新游戏,可以直接购买
  • 即将推出(coming_soon):还没发售的游戏,只能加愿望单

从用户心理来说,看新品是"买买买"的冲动,看即将推出是"期待期待"的心情。虽然 UI 结构相似,但业务含义不同。

价格显示的特殊处理

即将推出的游戏有个特点:很多还没定价

看一下 API 返回的数据:

{
  "id": 123456,
  "name": "Some Upcoming Game",
  "discounted": false,
  "discount_percent": 0,
  "original_price": null,
  "final_price": null,
  "header_image": "https://cdn.xxx/xxx.jpg"
}

注意 final_pricenull,不是 0。这和免费游戏不一样——免费游戏的 final_price0,而未定价游戏是 null

所以价格处理逻辑要调整:

price={game.final_price ? `¥${(game.final_price / 100).toFixed(2)}` : '即将发售'}

这里把默认文案从"免费"改成了"即将发售",更符合业务语义。

当然,有些即将推出的游戏已经定价了(可以预购),这时候 final_price 有值,就正常显示价格。

代码实现

直接看完整代码:

import React, {useEffect, useState} from 'react';
import {View, ScrollView, StyleSheet} from 'react-native';
import {Header} from '../components/Header';
import {TabBar} from '../components/TabBar';
import {GameCard} from '../components/GameCard';
import {Loading} from '../components/Loading';
import {getFeaturedCategories} from '../api/steam';

export const UpcomingScreen = () => {
  const [games, setGames] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    getFeaturedCategories().then(data => {
      setGames(data?.coming_soon?.items || []);
      setLoading(false);
    }).catch(() => setLoading(false));
  }, []);

  return (
    <View style={styles.container}>
      <Header title="即将推出" showBack />
      {loading ? <Loading /> : (
        <ScrollView style={styles.content}>
          {games.map((game: any) => (
            <GameCard
              key={game.id}
              appId={game.id}
              name={game.name}
              image={game.header_image}
              price={game.final_price ? `¥${(game.final_price / 100).toFixed(2)}` : '即将发售'}
            />
          ))}
        </ScrollView>
      )}
      <TabBar />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {flex: 1, backgroundColor: '#171a21'},
  content: {flex: 1, padding: 16},
});

和前几个页面对比,主要区别就两点:

  1. 数据路径是 data?.coming_soon?.items
  2. 价格默认文案是"即将发售"而不是"免费"

为什么不传 discount 属性

你可能注意到了,这个页面的 GameCard 没有传 discount 属性:

<GameCard
  key={game.id}
  appId={game.id}
  name={game.name}
  image={game.header_image}
  price={game.final_price ? `¥${(game.final_price / 100).toFixed(2)}` : '即将发售'}
  // 没有 discount={game.discount_percent}
/>

原因很简单:即将推出的游戏基本不会有折扣。它们还没发售呢,打什么折?

虽然 API 返回的数据里有 discount_percent 字段(值是 0),但传不传都一样。不传的话代码更简洁,也更能体现业务意图。

写代码不只是让程序跑起来,还要让读代码的人(包括未来的自己)能快速理解意图。

关于预购

Steam 上有些大作会开放预购,比如 3A 游戏在发售前几个月就能预购了。预购的游戏:

  • final_price 有值(预购价格)
  • 可能有预购折扣(discount_percent > 0)
  • 点击后跳转到详情页可以看到预购按钮

我们当前的实现已经能正确处理预购游戏:有价格就显示价格,有折扣就显示折扣标签。不需要额外改动。

点击卡片后的行为

用户点击即将推出的游戏卡片,会跳转到游戏详情页。这个逻辑在 GameCard 组件里:

const onPress = () => {
  setSelectedAppId(appId);
  addToHistory(appId);
  navigate('gameDetail');
};

详情页会调用 appdetails 接口获取完整信息,包括发售日期、游戏介绍、截图视频等。即将推出的游戏和已发售的游戏用的是同一个详情页,只是展示的内容略有不同(比如购买按钮变成愿望单按钮)。

这种设计的好处是复用。不需要为即将推出的游戏单独做一个详情页,减少了代码量和维护成本。

featuredcategories 系列回顾

到这里,featuredcategories 接口的四个分类都实现完了:

featuredcategories
├── specials        → 特惠游戏(第 03 篇)
├── top_sellers     → 热销榜(第 04 篇)
├── new_releases    → 新品上架(第 05 篇)
└── coming_soon     → 即将推出(本篇)

四个页面的代码结构几乎一样,区别只在于:

  • 数据提取路径不同
  • Header 标题不同
  • 价格默认文案不同(热销榜还多了排名显示)

这种"相似但有细微差异"的场景在实际开发中很常见。处理方式有两种:

  1. 保持独立:每个页面单独写,代码有重复但清晰
  2. 抽象复用:提取通用组件,通过参数控制差异

我们选择了方案一,因为代码量不大,而且各页面可能有不同的演进方向。如果你的项目有更多类似页面,方案二会更合适。

发售日期的展示思考

即将推出的游戏,用户最关心的信息之一就是发售日期。可惜 featuredcategories 接口返回的数据里没有这个字段。

如果想展示发售日期,需要额外调用 appdetails 接口:

export const getAppDetails = async (appId: number) => {
  const res = await fetch(`${STORE_API}/appdetails?appids=${appId}`);
  return res.json();
};

返回数据中有个 release_date 字段:

{
  "release_date": {
    "coming_soon": true,
    "date": "2024年第四季度"
  }
}

但这样做有个问题:列表里有多少游戏,就要调多少次接口。如果列表有 20 个游戏,就是 20 次请求,性能和体验都不好。

比较好的做法是:

  • 列表页不显示发售日期,保持简洁
  • 用户点进详情页后再展示完整信息

这也是我们当前的实现方式。

空状态处理

如果 API 返回的 coming_soon.items 是空数组怎么办?当前的实现会显示一个空白页面,用户体验不太好。

可以加个空状态提示:

{games.length === 0 ? (
  <View style={styles.emptyContainer}>
    <Text style={styles.emptyText}>暂无即将推出的游戏</Text>
  </View>
) : (
  <ScrollView style={styles.content}>
    {games.map((game: any) => (
      // ...
    ))}
  </ScrollView>
)}

不过实际上 Steam 的即将推出列表基本不会为空,总有新游戏在排队等发售。所以这个优化的优先级不高,但作为一个完善的 App,空状态处理是应该考虑的。

加载失败的处理

当前的错误处理比较简单:

.catch(() => setLoading(false));

请求失败时只是把 loading 设为 false,用户看到的是空列表,不知道发生了什么。

更友好的做法是加个错误状态:

const [error, setError] = useState(false);

useEffect(() => {
  getFeaturedCategories()
    .then(data => {
      setGames(data?.coming_soon?.items || []);
    })
    .catch(() => {
      setError(true);
    })
    .finally(() => {
      setLoading(false);
    });
}, []);

然后在渲染时判断:

{error ? (
  <View style={styles.errorContainer}>
    <Text style={styles.errorText}>加载失败</Text>
    <TouchableOpacity onPress={reload}>
      <Text style={styles.retryText}>点击重试</Text>
    </TouchableOpacity>
  </View>
) : (
  // 正常渲染
)}

这个优化在前面的文章也提过,这里再强调一下。生产环境的 App 一定要做好错误处理,不能让用户面对一个空白页面不知所措。

首页快捷入口的完善

回顾一下首页的快捷入口配置:

{[
  {name: 'featured', label: '精选', icon: '⭐'},
  {name: 'specials', label: '特惠', icon: '🏷️'},
  {name: 'topSellers', label: '热销', icon: '🔥'},
  {name: 'newReleases', label: '新品', icon: '🆕'},
].map(item => (
  <TouchableOpacity key={item.name} onPress={() => navigate(item.name)}>
    ...
  </TouchableOpacity>
))}

你会发现这里没有"即将推出"的入口。这是故意的——首页空间有限,只放最常用的四个入口。

如果想加上"即将推出",有几种方案:

  1. 把快捷入口从 4 个扩展到 5 个(可能会显得拥挤)
  2. 做成两行的宫格布局
  3. 放到"更多"菜单里

这些都是产品设计的选择,技术上都能实现。

页面跳转的路由配置

即将推出页面的路由名是 upcoming,在 App.tsx 的路由配置中:

const screens: Record<string, React.ReactNode> = {
  // 首页相关
  home: <HomeScreen />,
  featured: <FeaturedScreen />,
  specials: <SpecialsScreen />,
  topSellers: <TopSellersScreen />,
  newReleases: <NewReleasesScreen />,
  upcoming: <UpcomingScreen />,
  // ...其他页面
};

用户可以通过以下方式进入这个页面:

  1. 首页某个入口点击(如果配置了的话)
  2. 其他页面的链接跳转
  3. 深度链接(如果实现了的话)

我们的导航系统用的是简单的状态管理,通过 navigate('upcoming') 就能跳转。如果项目规模更大,可以考虑用 React Navigation 这样的专业导航库。

小结

即将推出页面的实现很简单,但背后有一些值得思考的点:

  • 业务语义:同样是"没有价格",免费游戏和未定价游戏的含义不同,显示文案也应该不同
  • 代码意图:不传 discount 属性不是忘了,而是业务上不需要
  • 复用思维:详情页不区分已发售和即将推出,减少重复代码

下一篇开始进入游戏详情相关的页面开发,内容会丰富很多。详情页需要展示游戏介绍、截图、视频、成就、新闻等信息,涉及多个 API 的调用和复杂的 UI 布局,敬请期待。


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

Logo

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

更多推荐