RN for OpenHarmony AnimeHub项目实战:放送时间表页面开发

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

放送时间表页展示每周各天的动漫播出安排,帮助用户追踪正在播出的动漫。这篇来讲放送时间表页的实现,重点是标签切换和日期计算。

功能设计

放送时间表页需要实现以下功能:

  • 星期标签 - 周一到周日七个标签,可切换
  • 默认今天 - 进入页面时自动选中今天
  • 动漫列表 - 显示选中日期的动漫列表
  • 实时切换 - 切换标签时加载对应数据

这个页面是追番用户的重要工具。日本动漫通常每周固定时间播出,用户需要知道今天有哪些动漫更新,才能及时观看。

放送时间表是动漫应用的核心功能之一。对于追番用户来说,知道"今天有什么更新"比"有什么好看的"更重要。这个页面直接影响用户的日常使用频率。
日本动漫的播出时间通常是日本时间(JST,UTC+9),和中国时间(CST,UTC+8)相差 1 小时。API 返回的是日本时间的安排,用户需要自己换算。

星期数据定义

定义星期的英文 key 和中文标签:

typescript 复制代码
const DAYS = [
  { key: 'monday', label: '周一' },
  { key: 'tuesday', label: '周二' },
  { key: 'wednesday', label: '周三' },
  { key: 'thursday', label: '周四' },
  { key: 'friday', label: '周五' },
  { key: 'saturday', label: '周六' },
  { key: 'sunday', label: '周日' },
];

数据结构说明:

  • key - 英文星期名,用于 API 请求
  • label - 中文标签,用于界面显示

为什么用数组而不是对象?因为我们需要保持顺序。数组的顺序是固定的(周一到周日),而对象的属性顺序在某些情况下可能不确定。
API 需要英文的星期名(monday、tuesday 等),但界面要显示中文。用这种 key-label 的结构可以方便地进行转换,代码也更清晰。
数组从周一开始而不是周日,是因为中国习惯周一是一周的开始。美国习惯周日是一周的开始,如果做国际化,可能需要调整顺序。

状态定义

typescript 复制代码
export const ScheduleScreen = ({ navigation }: any) => {
  const [animeList, setAnimeList] = useState<Anime[]>([]);
  const [loading, setLoading] = useState(true);
  const [selectedDay, setSelectedDay] = useState(DAYS[new Date().getDay() === 0 ? 6 : new Date().getDay() - 1].key);

状态说明:

  • animeList - 当前选中日期的动漫列表
  • loading - 加载状态
  • selectedDay - 当前选中的星期,默认是今天

默认选中今天的逻辑比较复杂,需要详细解释。

计算今天是星期几

typescript 复制代码
const [selectedDay, setSelectedDay] = useState(
  DAYS[new Date().getDay() === 0 ? 6 : new Date().getDay() - 1].key
);

计算逻辑详解:

  • new Date().getDay() 返回 0-6,其中 0 是周日,1 是周一,...,6 是周六
  • 我们的 DAYS 数组是从周一开始的,索引 0 是周一,索引 6 是周日
  • 需要把 JavaScript 的星期转换为我们数组的索引

转换公式:

  • 周日(getDay() = 0)→ 数组索引 6
  • 周一(getDay() = 1)→ 数组索引 0
  • 周二(getDay() = 2)→ 数组索引 1
  • ...
  • 周六(getDay() = 6)→ 数组索引 5

getDay() === 0 ? 6 : getDay() - 1 的逻辑:

  • 如果是周日(0),返回 6(数组最后一个)
  • 否则,返回 getDay() - 1(因为数组从周一开始,比 getDay 小 1)
    这是一个常见的日期处理问题。JavaScript 的 getDay() 把周日作为一周的开始(美国习惯),但我们的数组把周一作为开始(中国习惯),需要做转换。
    更清晰的写法可以是:
typescript 复制代码
const getTodayIndex = () => {
  const day = new Date().getDay();
  return day === 0 ? 6 : day - 1;
};
const [selectedDay, setSelectedDay] = useState(DAYS[getTodayIndex()].key);

数据加载函数

typescript 复制代码
const loadData = async (day: string) => {
  setLoading(true);
  try {
    const res = await getSchedule(day);
    setAnimeList(res.data || []);
  } catch (error) {
    console.error('Load error:', error);
  } finally {
    setLoading(false);
  }
};

加载逻辑:

  • 先设置 loading 为 true
  • 调用 API 获取指定日期的放送表
  • 成功时设置动漫列表
  • 失败时打印错误
  • 最终重置 loading

getSchedule(day) 是封装好的 API 函数,参数是英文星期名(如 'monday')。API 会返回该天播出的所有动漫。
这个页面没有分页,因为每天的动漫数量通常不会太多(几十部),可以一次性加载。如果数据量很大,可以考虑添加分页。

监听日期变化

typescript 复制代码
useEffect(() => {
  loadData(selectedDay);
}, [selectedDay]);

依赖说明:

  • 依赖 selectedDay
  • 当选中的日期变化时,重新加载数据

这是 React 的标准模式:状态变化触发副作用。用户点击标签 → selectedDay 变化 → useEffect 执行 → 加载新数据 → 界面更新。
初始渲染时也会执行一次,加载默认选中日期(今天)的数据。

列表项渲染

typescript 复制代码
const renderItem = ({ item }: { item: Anime }) => (
  <AnimeListItem
    anime={item}
    onPress={() => navigation.navigate('AnimeDetail', { animeId: item.mal_id })}
  />
);

渲染逻辑:

  • 使用 AnimeListItem 组件显示每个动漫
  • 点击跳转到详情页

放送时间表用列表布局而不是网格布局,因为用户更关心"有哪些动漫"而不是"封面好不好看"。列表布局可以显示更多信息(标题、集数、时间等)。

页面结构

typescript 复制代码
return (
  <View style={styles.container}>
    <Header title="放送时间表" showBack onBack={() => navigation.goBack()} />
    <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabs}>
      {DAYS.map(day => (
        <TouchableOpacity
          key={day.key}
          style={[styles.tab, selectedDay === day.key && styles.tabActive]}
          onPress={() => setSelectedDay(day.key)}
        >
          <Text style={[styles.tabText, selectedDay === day.key && styles.tabTextActive]}>
            {day.label}
          </Text>
        </TouchableOpacity>
      ))}
    </ScrollView>

页面组成:

  • Header 显示标题和返回按钮
  • 横向滚动的星期标签
  • 动漫列表

星期标签用 ScrollView horizontal 实现横向滚动。虽然七个标签在大多数手机上可以一行显示,但在小屏幕设备上可能放不下,横向滚动可以保证所有标签都能访问。

标签渲染详解

typescript 复制代码
{DAYS.map(day => (
  <TouchableOpacity
    key={day.key}
    style={[styles.tab, selectedDay === day.key && styles.tabActive]}
    onPress={() => setSelectedDay(day.key)}
  >
    <Text style={[styles.tabText, selectedDay === day.key && styles.tabTextActive]}>
      {day.label}
    </Text>
  </TouchableOpacity>
))}

渲染逻辑:

  • 遍历 DAYS 数组,为每个星期生成一个标签
  • key={day.key} 用英文星期名作为 React 的 key
  • 样式根据是否选中动态变化
  • 点击时更新 selectedDay 状态

style={[styles.tab, selectedDay === day.key && styles.tabActive]} 是条件样式的写法。数组中的样式会合并,当条件为 false 时,&& 返回 false,React Native 会忽略它。
这种写法比三元运算符更简洁:

typescript 复制代码
// 三元运算符写法(更长)
style={selectedDay === day.key ? [styles.tab, styles.tabActive] : styles.tab}
// && 写法(更短)
style={[styles.tab, selectedDay === day.key && styles.tabActive]}

条件渲染列表

typescript 复制代码
    {loading ? (
      <Loading fullScreen text="加载中..." />
    ) : (
      <FlatList
        data={animeList}
        renderItem={renderItem}
        keyExtractor={item => item.mal_id.toString()}
        contentContainerStyle={styles.list}
        showsVerticalScrollIndicator={false}
        ListEmptyComponent={<EmptyState icon="calendar" title="暂无数据" />}
      />
    )}
  </View>
);

两种状态:

  • 加载中显示 Loading
  • 加载完成显示列表或空状态

空状态用日历图标(calendar),和时间表的概念相关。空状态可能出现在某些冷门的日期,比如某天确实没有动漫播出。

标签样式

typescript 复制代码
tabs: {
  flexGrow: 0,
  paddingHorizontal: Spacing.md,
  paddingVertical: Spacing.sm,
  borderBottomWidth: 1,
  borderBottomColor: Colors.border,
},

样式说明:

  • flexGrow: 0 防止 ScrollView 占据所有空间
  • 水平和垂直内边距
  • 底部有分隔线

flexGrow: 0 很重要。ScrollView 默认会尽可能占据空间,设置 flexGrow: 0 让它只占据内容需要的高度,剩余空间留给下面的 FlatList。
底部分隔线用 borderBottomWidthborderBottomColor 实现,视觉上把标签区域和列表区域分开。

typescript 复制代码
tab: {
  paddingHorizontal: Spacing.lg,
  paddingVertical: Spacing.sm,
  marginRight: Spacing.sm,
  borderRadius: BorderRadius.full,
  backgroundColor: Colors.backgroundLight,
},
tabActive: {
  backgroundColor: Colors.primary,
},

标签样式:

  • 胶囊形状(borderRadius: BorderRadius.full
  • 未选中时用浅色背景
  • 选中时用主题色背景
  • 标签之间有间距

BorderRadius.full 是一个很大的圆角值(如 9999),可以让矩形变成胶囊形状。这是一个常用的技巧,不需要计算具体的圆角值。
胶囊形状的标签是现代 UI 的流行设计,比方形标签更柔和,比圆形标签更能容纳文字。

typescript 复制代码
tabText: {
  fontSize: FontSize.md,
  color: Colors.textSecondary,
  fontWeight: '500',
},
tabTextActive: {
  color: Colors.text,
},

文字样式:

  • 未选中时用次要颜色(灰色)
  • 选中时用主要颜色(白色或亮色)
  • 稍微加粗(500)

选中和未选中的视觉差异要明显,让用户一眼就能看出当前选中的是哪个。背景色变化 + 文字颜色变化,双重反馈。

列表样式

typescript 复制代码
list: {
  padding: Spacing.md,
},

样式说明:

  • 列表四周有内边距

样式非常简洁,因为大部分样式都在 AnimeListItem 组件内部。页面只需要关心整体布局。

时区问题

放送时间表涉及时区问题:

  • Jikan API 返回的是日本时间(JST,UTC+9)
  • 中国用户使用的是北京时间(CST,UTC+8)
  • 两者相差 1 小时

举个例子:如果一部动漫在日本时间周六 24:00(即周日 0:00)播出,对于中国用户来说是周六 23:00。但 API 可能把它归类到周日。
这个问题在当前实现中没有处理,用户看到的是日本时间的安排。如果要做得更好,可以:

  1. 在界面上提示"以下为日本时间"
  2. 或者在客户端做时区转换

与新番页的区别

放送时间表和新番页都展示当季动漫,但角度不同:

  • 新番页 - 按季度展示,关注"这个季度有什么"
  • 放送时间表 - 按星期展示,关注"今天有什么更新"

新番页适合在季度开始时浏览,了解这个季度有哪些新作品。放送时间表适合日常使用,查看今天有哪些动漫更新。
两个页面的数据有重叠(都是当季正在播出的动漫),但组织方式不同,满足不同的使用场景。

追番场景

放送时间表的典型使用场景:

  1. 用户打开应用,想知道今天有什么更新
  2. 页面自动显示今天的放送表
  3. 用户看到自己追的动漫在列表中
  4. 点击进入详情页,查看最新一集的信息

对于重度追番用户,这个页面可能是每天都会访问的。设计时要考虑高频使用的体验,比如默认显示今天、加载速度要快。

可能的优化

当前实现可以进一步优化:

  • 缓存数据 - 同一天的数据可以缓存,避免重复请求
  • 显示播出时间 - 在列表项中显示具体的播出时间
  • 标记已追 - 高亮显示用户收藏的动漫
  • 时区转换 - 把日本时间转换为本地时间

缓存可以用 React Query 或自己实现。放送时间表的数据在一天内不会变化,非常适合缓存。
标记已追是一个很好的功能。用户收藏的动漫在时间表中高亮显示,可以快速找到自己关心的内容。

小结

放送时间表页展示每周各天的动漫播出安排,是追番用户的重要工具。页面使用横向滚动的标签切换星期,默认选中今天。

日期计算是这个页面的难点。JavaScript 的 getDay() 返回 0-6(周日到周六),需要转换为我们数组的索引(周一到周日)。转换公式是 day === 0 ? 6 : day - 1

标签使用胶囊形状,选中和未选中有明显的视觉差异。切换标签时触发数据加载,useEffect 监听 selectedDay 的变化。

放送时间表和新番页互补,一个按星期组织,一个按季度组织,满足不同的使用场景。

下一篇会讲正在热播页面,展示当前正在播出的热门动漫。


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

相关推荐
橘子真甜~2 小时前
Reids命令原理与应用5 - Redis 主从同步与高可用集群
运维·网络·数据库·redis·缓存·redis集群·redis高可用
松涛和鸣2 小时前
DAY52 7-Segment Display/GPIO/Buttons/Interrupts/Timers/PWM
c语言·数据库·单片机·sqlite·html
想摆烂的不会研究的研究生2 小时前
每日八股——Redis(3)
数据库·redis·后端·缓存
寂寞恋上夜2 小时前
数据迁移方案怎么写:迁移策略/回滚方案/验证方法(附完整模板)
网络·数据库·oracle·markdown转xmind·deepseek思维导图
冉冰学姐2 小时前
SSM校园学习空间预约系统w314l(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·学习·ssm 框架·校园学习空间预约系统·师生双角色
360智汇云2 小时前
HULK PostgreSQL 图数据库化方案:Apache AGE 的引入与实践
数据库·postgresql·apache
Full Stack Developme3 小时前
Redis 实现主从同步
java·redis·spring
SelectDB技术团队3 小时前
驾驭 CPU 与编译器:Apache Doris 实现极致性能的底层逻辑
数据库·数据仓库·人工智能·sql·apache
万邦科技Lafite3 小时前
阿里巴巴商品详情API返回值:电商精准营销的关键
大数据·数据库·人工智能·电商开放平台