在这里插入图片描述

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

全部季度页展示所有年份和季度,用户可以按时间维度浏览动漫。这篇来讲全部季度页的实现,重点是嵌套数据结构的渲染和 Emoji 图标的使用。

功能设计

全部季度页需要实现以下功能:

  • 年份分组 - 按年份分组显示,每年有四个季度
  • 季度卡片 - 用 Emoji 图标表示不同季节
  • 点击跳转 - 点击季度跳转到该季度的动漫列表
  • 时间排序 - 最新的年份在最上面

这个页面的布局比较特殊,不是简单的列表或网格,而是按年份分组的嵌套结构。每个年份下面有四个季度卡片横向排列,用户可以快速定位到想看的时间段。这种设计让时间维度的浏览变得直观高效。

季节名称映射

定义季节的中文名称和 Emoji 图标:

const SEASON_NAMES: Record<string, string> = {
  winter: '冬季',
  spring: '春季',
  summer: '夏季',
  fall: '秋季',
};

名称映射说明:

  • winter → 冬季(1-3月)
  • spring → 春季(4-6月)
  • summer → 夏季(7-9月)
  • fall → 秋季(10-12月)

Record<string, string> 类型定义映射对象。这是 TypeScript 的内置工具类型,表示一个对象,key 是 string 类型,value 也是 string 类型。比起普通的 { [key: string]: string },Record 更简洁易读。

为什么需要这个映射?因为 API 返回的是英文季节名(winter、spring 等),但我们想在界面上显示中文。用映射对象可以方便地进行转换,而且集中管理,修改时只需要改一处。

const SEASON_ICONS: Record<string, string> = {
  winter: '❄️',
  spring: '🌸',
  summer: '☀️',
  fall: '🍂',
};

图标映射说明:

  • winter → ❄️(雪花,代表冬天的寒冷)
  • spring → 🌸(樱花,日本春天的象征)
  • summer → ☀️(太阳,代表夏天的炎热)
  • fall → 🍂(落叶,代表秋天的萧瑟)

Emoji 图标是一个非常聪明的设计选择,有以下优点:

  1. 零资源成本 - 不需要额外的图标文件,减少包体积
  2. 跨平台一致 - iOS、Android、鸿蒙都支持 Emoji
  3. 视觉直观 - 用户一眼就能理解每个季节
  4. 增加趣味性 - 比纯文字更生动有趣

樱花 🌸 的选择特别贴切。樱花是日本文化的象征,每年春天的樱花季是日本的重要时节。用樱花代表春季,和动漫的日本文化背景非常搭配。

状态定义

const [seasons, setSeasons] = useState<Season[]>([]);
const [loading, setLoading] = useState(true);

状态说明:

  • seasons - 季度列表数据,是一个数组,每项包含年份和该年的季度
  • loading - 加载状态,控制 Loading 组件的显示

这个页面的状态非常简单,只有数据和加载状态。不需要分页状态,因为季度数据是按年份组织的,总数据量不大(从 1960 年代到现在,大约 60 年的数据),可以一次性加载完。

简单的状态设计意味着页面逻辑简单,bug 也会更少。能用简单方案解决的问题,就不要用复杂方案。

数据类型

季度数据的结构是嵌套的:

interface Season {
  year: number;
  seasons: string[];
}

字段详细说明:

  • year - 年份,数字类型,如 2024、2023、2022
  • seasons - 该年的季度数组,字符串数组,如 ['winter', 'spring', 'summer', 'fall']

这个数据结构是"分组"模式的典型例子。外层按年份分组,内层是该年的季度列表。这种结构非常适合我们的 UI 设计:每个年份是一个区块,区块内显示四个季度卡片。

有些年份可能不是四个季度都有数据。比如很早的年份(1960 年代),可能只有部分季度有动漫播出。API 只返回有数据的季度,所以 seasons 数组的长度可能是 1-4。

数据加载

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

const loadSeasons = async () => {
  try {
    const data = await getSeasonsList();
    setSeasons(data.data || []);
  } catch (error) {
    console.error('Load seasons error:', error);
  } finally {
    setLoading(false);
  }
};

加载逻辑详解:

  • useEffect 的依赖数组为空,表示只在组件挂载时执行一次
  • getSeasonsList 是封装好的 API 函数,返回所有年份和季度的列表
  • data.data || [] 处理 API 返回空数据的情况
  • 错误时打印日志,不会导致页面崩溃
  • finally 确保无论成功失败都会把 loading 设为 false

季度数据是相对固定的,只有当新的季度开始时才会更新(每三个月一次)。这个 API 的响应非常适合做本地缓存,可以减少网络请求,提升用户体验。

为什么不需要分页?因为数据量可控。假设从 1960 年到 2024 年,共 64 年,每年一条数据,总共只有 64 条。这个数据量 FlatList 完全可以轻松处理。

年份和季度渲染

这是这个页面最核心的部分,实现嵌套渲染:

const renderItem = ({ item }: { item: Season }) => (
  <View style={styles.yearSection}>
    <Text style={styles.yearTitle}>{item.year}</Text>
    <View style={styles.seasonsRow}>
      {item.seasons.map((season) => (
        <TouchableOpacity
          key={season}
          style={styles.seasonCard}
          onPress={() => navigation.navigate('SeasonAnime', { 
            year: item.year, 
            season 
          })}
        >
          <Text style={styles.seasonIcon}>{SEASON_ICONS[season]}</Text>
          <Text style={styles.seasonName}>{SEASON_NAMES[season]}</Text>
        </TouchableOpacity>
      ))}
    </View>
  </View>
);

渲染逻辑详解:

  • 外层 yearSection 是年份区块,包含年份标题和季度卡片行
  • yearTitle 显示年份数字,如"2024"
  • seasonsRow 是季度卡片的容器,横向排列
  • 内层用 map 遍历该年的季度数组,生成季度卡片
  • 每个卡片显示 Emoji 图标和中文季节名
  • 点击卡片跳转到季度动漫页面

这是一个典型的嵌套渲染模式:FlatList 负责渲染年份列表(外层循环),每个年份内部用 map 渲染季度卡片(内层循环)。这种模式适合"分组 + 子项"的数据结构。

key={season} 使用季节名作为 key。因为同一年内季节名是唯一的(不会有两个 winter),所以可以作为 key。

navigation.navigate('SeasonAnime', { year: item.year, season }) 传递两个参数:

  • year - 年份数字,如 2024
  • season - 季节英文名,如 ‘spring’

下一个页面会用这两个参数请求该季度的动漫列表。

年份区块样式

yearSection: {
  marginBottom: Spacing.xl,
},
yearTitle: {
  fontSize: FontSize.xxl,
  fontWeight: '700',
  color: Colors.text,
  marginBottom: Spacing.md,
},

样式详解:

  • 每个年份区块底部有较大间距(xl),让不同年份之间有明显分隔
  • 年份标题用超大字号(xxl,可能是 24-28px),加粗
  • 标题和下面的季度卡片之间有中等间距(md)

年份标题用超大字号是有意为之的设计。当用户快速滚动页面时,大字号的年份数字非常醒目,可以帮助用户快速定位到想要的年份。这是一种"扫描友好"的设计。

fontWeight: '700' 是加粗。在 CSS/React Native 中,字重从 100(最细)到 900(最粗),700 相当于 bold。

季度卡片样式

seasonsRow: {
  flexDirection: 'row',
  gap: Spacing.md,
},
seasonCard: {
  flex: 1,
  backgroundColor: Colors.backgroundCard,
  padding: Spacing.lg,
  borderRadius: BorderRadius.lg,
  alignItems: 'center',
},

样式详解:

  • flexDirection: 'row' 让四个季度卡片横向排列
  • gap: Spacing.md 设置卡片之间的间距(gap 是 Flexbox 的属性,比 margin 更方便)
  • flex: 1 让每个卡片平分父容器的宽度
  • 卡片有背景色、内边距、圆角
  • alignItems: 'center' 让卡片内容(图标和文字)水平居中

flex: 1 是实现等宽布局的关键。当四个子元素都设置 flex: 1 时,它们会平分父容器的可用空间。这比手动计算百分比(如 width: ‘25%’)更灵活,因为它会自动处理间距。

假设父容器宽度是 343px(iPhone SE 屏幕宽度 375 - 左右边距 32),四个卡片加三个间距。如果间距是 12px,那么:

  • 可用宽度 = 343 - 12 * 3 = 307px
  • 每个卡片宽度 = 307 / 4 ≈ 77px
seasonIcon: {
  fontSize: 28,
  marginBottom: Spacing.sm,
},
seasonName: {
  fontSize: FontSize.sm,
  color: Colors.textSecondary,
},

卡片内容样式:

  • Emoji 图标用 28 号字体,足够大,视觉上醒目
  • 图标和文字之间有小间距
  • 季节名称用小字号,次要颜色

Emoji 的 fontSize 和普通文字一样设置。28px 的 Emoji 在移动端显示效果很好,既能看清细节,又不会占用太多空间。

季节名称用次要颜色(textSecondary,通常是灰色),是因为 Emoji 图标已经足够表达季节含义,文字只是辅助说明,不需要太突出。

页面结构

return (
  <View style={styles.container}>
    <Header
      title="季度列表"
      showBack
      onBack={() => navigation.goBack()}
    />

Header 配置:

  • 标题显示"季度列表",简洁明了
  • 显示返回按钮,点击返回上一页

条件渲染

    {loading ? (
      <Loading fullScreen />
    ) : (
      <FlatList
        data={seasons}
        renderItem={renderItem}
        keyExtractor={(item) => item.year.toString()}
        contentContainerStyle={styles.list}
        showsVerticalScrollIndicator={false}
      />
    )}
  </View>
);

两种状态的处理:

  • 加载中显示全屏 Loading
  • 加载完成显示年份列表

这里没有空状态处理,因为季度数据不可能为空。动漫从 1960 年代就开始有了,API 一定会返回数据。如果 API 出错,会在 catch 中处理,页面会显示空白列表。

FlatList 配置详解

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

配置说明:

  • data={seasons} - 数据源是年份数组
  • renderItem={renderItem} - 渲染函数
  • keyExtractor - 用年份作为 key,年份是唯一的
  • contentContainerStyle - 内容容器样式
  • showsVerticalScrollIndicator={false} - 隐藏滚动条

注意这里没有 numColumns,因为我们不是要做网格布局。每个列表项(年份区块)内部自己处理了四列布局(用 flexDirection: ‘row’)。FlatList 只负责纵向滚动年份列表。

也没有分页相关的配置(onEndReached 等),因为数据量不大,一次性加载即可。

list: {
  padding: Spacing.lg,
},

列表样式:

  • 四周有内边距,内容不贴边

与新番页的关系

全部季度页和新番页(SeasonalScreen)功能有重叠,但定位不同:

  • 新番页 - 显示当前季度的动漫,可以切换年份和季度,适合浏览最新内容
  • 全部季度页 - 显示所有年份和季度的入口,适合查找特定时间段的动漫

举个例子:

  • 用户想看"这个季度有什么新番" → 用新番页
  • 用户想看"2019年春季有什么好看的动漫" → 用全部季度页

两个页面互补,满足不同的使用场景。新番页更适合"追新",全部季度页更适合"考古"。

季度动漫的时间规律

日本动漫的播出遵循固定的季度规律:

  • 冬季(Winter) - 1月开播,1-3月播出
  • 春季(Spring) - 4月开播,4-6月播出
  • 夏季(Summer) - 7月开播,7-9月播出
  • 秋季(Fall) - 10月开播,10-12月播出

每个季度大约有 30-50 部新番开播。一部标准的季番(12-13集)正好在一个季度内播完。有些作品是两季连播(24-26集),会跨越两个季度。

了解这个规律对用户很有帮助。比如现在是 2024 年 5 月,那么当前季度是"2024年春季",用户可以在这个季度找到正在播出的新番。

小结

全部季度页的核心是嵌套数据结构的渲染和 Emoji 图标的使用。FlatList 渲染年份列表,每个年份内部用 map 渲染四个季度卡片,形成清晰的层次结构。

Emoji 图标是一个巧妙的设计选择,零资源成本,跨平台兼容,视觉直观,还能增加页面的趣味性。季节名称用映射对象从英文转换为中文,代码集中管理,易于维护。

年份标题用超大字号显示,方便用户快速扫描定位。四个季度卡片用 flex: 1 平分宽度,布局简洁灵活,自动适应不同屏幕宽度。

下一篇会讲季度动漫页面,展示某个季度的所有动漫。


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

Logo

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

更多推荐