
放送时间表页展示每周各天的动漫播出安排,帮助用户追踪正在播出的动漫。这篇来讲放送时间表页的实现,重点是标签切换和日期计算。
功能设计
放送时间表页需要实现以下功能:
- 星期标签 - 周一到周日七个标签,可切换
- 默认今天 - 进入页面时自动选中今天
- 动漫列表 - 显示选中日期的动漫列表
- 实时切换 - 切换标签时加载对应数据
这个页面是追番用户的重要工具。日本动漫通常每周固定时间播出,用户需要知道今天有哪些动漫更新,才能及时观看。
放送时间表是动漫应用的核心功能之一。对于追番用户来说,知道"今天有什么更新"比"有什么好看的"更重要。这个页面直接影响用户的日常使用频率。
日本动漫的播出时间通常是日本时间(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() 把周日作为一周的开始(美国习惯),但我们的数组把周一作为开始(中国习惯),需要做转换。
更清晰的写法可以是:
typescriptconst 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。
底部分隔线用borderBottomWidth和borderBottomColor实现,视觉上把标签区域和列表区域分开。
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 可能把它归类到周日。
这个问题在当前实现中没有处理,用户看到的是日本时间的安排。如果要做得更好,可以:
- 在界面上提示"以下为日本时间"
- 或者在客户端做时区转换
与新番页的区别
放送时间表和新番页都展示当季动漫,但角度不同:
- 新番页 - 按季度展示,关注"这个季度有什么"
- 放送时间表 - 按星期展示,关注"今天有什么更新"
新番页适合在季度开始时浏览,了解这个季度有哪些新作品。放送时间表适合日常使用,查看今天有哪些动漫更新。
两个页面的数据有重叠(都是当季正在播出的动漫),但组织方式不同,满足不同的使用场景。
追番场景
放送时间表的典型使用场景:
- 用户打开应用,想知道今天有什么更新
- 页面自动显示今天的放送表
- 用户看到自己追的动漫在列表中
- 点击进入详情页,查看最新一集的信息
对于重度追番用户,这个页面可能是每天都会访问的。设计时要考虑高频使用的体验,比如默认显示今天、加载速度要快。
可能的优化
当前实现可以进一步优化:
- 缓存数据 - 同一天的数据可以缓存,避免重复请求
- 显示播出时间 - 在列表项中显示具体的播出时间
- 标记已追 - 高亮显示用户收藏的动漫
- 时区转换 - 把日本时间转换为本地时间
缓存可以用 React Query 或自己实现。放送时间表的数据在一天内不会变化,非常适合缓存。
标记已追是一个很好的功能。用户收藏的动漫在时间表中高亮显示,可以快速找到自己关心的内容。
小结
放送时间表页展示每周各天的动漫播出安排,是追番用户的重要工具。页面使用横向滚动的标签切换星期,默认选中今天。
日期计算是这个页面的难点。JavaScript 的 getDay() 返回 0-6(周日到周六),需要转换为我们数组的索引(周一到周日)。转换公式是 day === 0 ? 6 : day - 1。
标签使用胶囊形状,选中和未选中有明显的视觉差异。切换标签时触发数据加载,useEffect 监听 selectedDay 的变化。
放送时间表和新番页互补,一个按星期组织,一个按季度组织,满足不同的使用场景。
下一篇会讲正在热播页面,展示当前正在播出的热门动漫。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net