React Native鸿蒙跨平台剧集管理应用实现,包含主应用组件、剧集列表、分类筛选、搜索排序等功能模块
本文探讨了一个基于React Native的剧集管理应用实现,重点分析了其架构设计、状态管理和数据处理策略。应用采用组件化架构,包含主应用组件、剧集列表、分类筛选、搜索排序等功能模块,实现了清晰的功能分离。使用useState管理剧集列表、分类、搜索等状态,并通过TypeScript确保类型安全。应用支持多种分类方式、多维排序和内容搜索功能,数据组织合理,界面布局层次分明。文章还提出了性能优化建议
在移动应用开发中,媒体内容管理应用是一种常见的应用类型,需要考虑内容展示、分类筛选、搜索排序等多个方面。本文将深入分析一个功能完备的 React Native 剧集管理应用实现,探讨其架构设计、状态管理、数据处理以及跨端兼容性策略。
组件化
该实现采用了清晰的组件化架构,主要包含以下部分:
- 主应用组件 (
RickAndMortyApp) - 负责整体布局和状态管理 - 剧集列表渲染 - 负责渲染剧集卡片列表
- 分类筛选 - 提供按季和特殊分类筛选剧集的功能
- 搜索功能 - 提供搜索剧集的功能
- 排序功能 - 提供按集数、日期、评分排序的功能
- 过滤功能 - 提供按观看状态筛选的功能
这种架构设计使得代码结构清晰,易于维护和扩展。主应用组件负责管理全局状态和业务逻辑,而各个功能部分负责具体的 UI 渲染,实现了关注点分离。
状态管理
RickAndMortyApp 组件使用 useState 钩子管理多个关键状态:
const [episodeList, setEpisodeList] = useState<Episode[]>(episodes);
const [activeCategory, setActiveCategory] = useState<string>('1');
const [searchQuery, setSearchQuery] = useState<string>('');
const [sortBy, setSortBy] = useState<'number' | 'date' | 'rating'>('number');
const [filterViewed, setFilterViewed] = useState<boolean>(false);
这种状态管理方式简洁高效,通过状态更新触发组件重新渲染,实现了剧集的筛选、搜索、排序等功能。使用 TypeScript 类型定义确保了数据结构的类型安全,提高了代码的可靠性。
剧集分类与筛选
应用实现了灵活的剧集分类与筛选功能:
// 获取当前分类的剧集
const getCurrentCategoryEpisodes = () => {
if (activeCategory === '7') {
// 最喜爱
return episodeList.filter(ep => ep.rating >= 9.0);
} else if (activeCategory === '8') {
// 未观看
return episodeList.filter(ep => !ep.viewed);
} else {
const seasonNum = parseInt(categories.find(cat => cat.id === activeCategory)?.name.replace('季', '') || '1');
return episodeList.filter(ep => ep.season === seasonNum);
}
};
这种实现方式支持多种分类类型:
- 按季分类 - 根据剧集的 season 属性筛选
- 特殊分类 - 最喜爱(评分≥9.0)和未观看(viewed=false)
排序功能
应用实现了多维度的排序功能:
// 按排序方式整理数据
const sortedEpisodes = getCurrentCategoryEpisodes().sort((a, b) => {
switch (sortBy) {
case 'number':
return a.season !== b.season ? a.season - b.season : a.episodeNumber - b.episodeNumber;
case 'date':
return new Date(a.airDate).getTime() - new Date(b.airDate).getTime();
case 'rating':
return b.rating - a.rating;
default:
return 0;
}
});
这种实现方式支持三种排序方式:
- 按集数排序 - 先按季排序,再按集数排序
- 按日期排序 - 按播出日期排序
- 按评分排序 - 按评分降序排序
搜索功能
应用实现了剧集搜索功能,通过过滤排序后的剧集列表:
.filter(episode =>
searchQuery === '' ||
episode.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
episode.description.toLowerCase().includes(searchQuery.toLowerCase())
)
这种实现方式支持按标题和描述搜索剧集,提高了用户查找特定剧集的效率。
类型定义
该实现使用 TypeScript 定义了两个核心数据类型:
- Episode - 剧集类型,包含剧集的完整信息,如 ID、季、集数、标题、播出日期、描述、时长、观看状态和评分
- Category - 分类类型,包含分类的 ID、名称和图标
这些类型定义使得数据结构更加清晰,提高了代码的可读性和可维护性,同时也提供了类型安全保障。
数据组织
应用数据按照功能模块进行组织:
- episodes - 剧集列表
- categories - 分类列表
- episodeList - 当前管理的剧集集合
- activeCategory - 当前选中的分类
- searchQuery - 搜索关键字
- sortBy - 排序方式
- filterViewed - 是否过滤已观看的剧集
这种数据组织方式使得数据管理更加清晰,易于扩展和维护。
布局结构
应用界面采用了清晰的层次结构:
- 顶部 - 显示应用标题和搜索栏
- 分类筛选 - 显示分类列表,允许用户选择分类
- 功能栏 - 显示排序、筛选等功能按钮
- 剧集列表 - 显示当前筛选条件下的剧集列表
这种布局结构符合用户的使用习惯,用户可以快速了解应用内容并进行操作。
当前实现使用 FlatList 渲染剧集列表,这是一个好的做法,但可以进一步优化:
// 优化前
<FlatList
data={sortedEpisodes}
renderItem={({ item }) => (
<EpisodeCard episode={item} />
)}
keyExtractor={item => item.id}
/>
// 优化后
<FlatList
data={sortedEpisodes}
renderItem={({ item }) => (
<EpisodeCard episode={item} />
)}
keyExtractor={item => item.id}
initialNumToRender={5} // 初始渲染的项目数
maxToRenderPerBatch={10} // 每批渲染的最大项目数
windowSize={10} // 可见区域外渲染的项目数
removeClippedSubviews={true} // 移除不可见的子视图
updateCellsBatchingPeriod={100} // 单元格更新的批处理周期
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT, // 预计算的项目高度
offset: ITEM_HEIGHT * index,
index
})}
/>
2. 状态管理
当前实现使用多个 useState 钩子管理状态,可以考虑使用 useReducer 或状态管理库来管理复杂状态:
// 优化前
const [episodeList, setEpisodeList] = useState<Episode[]>(episodes);
const [activeCategory, setActiveCategory] = useState<string>('1');
const [searchQuery, setSearchQuery] = useState<string>('');
const [sortBy, setSortBy] = useState<'number' | 'date' | 'rating'>('number');
const [filterViewed, setFilterViewed] = useState<boolean>(false);
// 优化后
type AppState = {
episodeList: Episode[];
activeCategory: string;
searchQuery: string;
sortBy: 'number' | 'date' | 'rating';
filterViewed: boolean;
};
type AppAction =
| { type: 'SET_EPISODE_LIST'; payload: Episode[] }
| { type: 'SET_ACTIVE_CATEGORY'; payload: string }
| { type: 'SET_SEARCH_QUERY'; payload: string }
| { type: 'SET_SORT_BY'; payload: 'number' | 'date' | 'rating' }
| { type: 'SET_FILTER_VIEWED'; payload: boolean }
| { type: 'TOGGLE_VIEWED'; payload: string };
const initialState: AppState = {
episodeList: episodes,
activeCategory: '1',
searchQuery: '',
sortBy: 'number',
filterViewed: false
};
const appReducer = (state: AppState, action: AppAction): AppState => {
switch (action.type) {
case 'SET_EPISODE_LIST':
return { ...state, episodeList: action.payload };
case 'SET_ACTIVE_CATEGORY':
return { ...state, activeCategory: action.payload };
case 'SET_SEARCH_QUERY':
return { ...state, searchQuery: action.payload };
case 'SET_SORT_BY':
return { ...state, sortBy: action.payload };
case 'SET_FILTER_VIEWED':
return { ...state, filterViewed: action.payload };
case 'TOGGLE_VIEWED':
return {
...state,
episodeList: state.episodeList.map(episode =>
episode.id === action.payload ? { ...episode, viewed: !episode.viewed } : episode
)
};
default:
return state;
}
};
const [state, dispatch] = useReducer(appReducer, initialState);
3. 数据持久化
当前实现使用内存状态存储数据,可以考虑集成本地存储实现数据持久化:
import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEYS = {
EPISODE_LIST: '@episode_list',
SETTINGS: '@app_settings',
};
const RickAndMortyApp = () => {
const [episodeList, setEpisodeList] = useState<Episode[]>(episodes);
const [settings, setSettings] = useState({
activeCategory: '1',
sortBy: 'number' as 'number' | 'date' | 'rating',
filterViewed: false,
});
// 加载数据
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
const storedEpisodes = await AsyncStorage.getItem(STORAGE_KEYS.EPISODE_LIST);
const storedSettings = await AsyncStorage.getItem(STORAGE_KEYS.SETTINGS);
if (storedEpisodes) {
setEpisodeList(JSON.parse(storedEpisodes));
}
if (storedSettings) {
setSettings(JSON.parse(storedSettings));
}
} catch (error) {
console.error('加载数据失败:', error);
}
};
// 保存数据
const saveData = async () => {
try {
await AsyncStorage.setItem(STORAGE_KEYS.EPISODE_LIST, JSON.stringify(episodeList));
await AsyncStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(settings));
} catch (error) {
console.error('保存数据失败:', error);
}
};
// 当数据变化时保存
useEffect(() => {
saveData();
}, [episodeList, settings]);
// 其他代码...
};
4. 导航系统
可以集成 React Navigation 实现剧集详情页面的导航:
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={RickAndMortyApp}
options={{ title: 'Rick and Morty' }}
/>
<Stack.Screen
name="EpisodeDetail"
component={EpisodeDetailScreen}
options={({ route }) => ({ title: route.params?.episodeTitle || '剧集详情' })}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
const EpisodeDetailScreen = ({ route }: { route: any }) => {
const { episodeId } = route.params;
// 获取剧集详情并渲染
return (
<View style={styles.detailContainer}>
{/* 剧集详情内容 */}
</View>
);
};
本文深入分析了一个功能完备的 React Native 剧集管理应用实现,从架构设计、状态管理、数据处理到跨端兼容性都进行了详细探讨。该实现不仅功能完整,而且代码结构清晰,具有良好的可扩展性和可维护性。
理解这个功能完整的 React Native 瑞克和莫蒂剧集库应用的技术实现逻辑,同时掌握其向鸿蒙(HarmonyOS)平台跨端适配的核心思路与具体实现方案。该应用作为典型的影视类列表应用,涵盖了多维度筛选、状态管理、复杂排序、交互反馈等移动端开发的核心场景,是跨端开发学习的优质案例。
1. 应用架构
该瑞克和莫蒂剧集库应用采用状态驱动+多维度数据处理的现代化 React Native 架构,核心数据模型设计充分体现了影视类应用的业务特征:
// 剧集核心数据模型 - 覆盖影视类应用核心属性
type Episode = {
id: string; // 唯一标识
season: number; // 季数
episodeNumber: number; // 集数
title: string; // 标题
airDate: string; // 播出日期
description: string; // 剧情描述
duration: string; // 时长
viewed: boolean; // 观看状态
rating: number; // 评分
};
// 分类模型 - 用于多维度筛选
type Category = {
id: string;
name: string;
icon: string; // 分类图标(emoji形式)
};
数据模型设计亮点:
- 业务属性完整性:涵盖影视类应用必备的基础信息、状态标识、评分体系;
- 状态标识清晰:
viewed布尔字段明确标识观看状态,支持用户交互; - 分级体系合理:通过
season和episodeNumber实现剧集的层级管理; - 分类扩展性强:独立的
Category模型支持常规分类(季数)和特殊分类(最喜爱、未观看)。
(1)多维度数据筛选
应用的核心价值在于支持复杂的多维度数据筛选,这是影视类应用的典型需求:
// 核心状态定义 - 控制筛选和排序行为
const [episodeList, setEpisodeList] = useState<Episode[]>(episodes);
const [activeCategory, setActiveCategory] = useState<string>('1');
const [searchQuery, setSearchQuery] = useState<string>('');
const [sortBy, setSortBy] = useState<'number' | 'date' | 'rating'>('number');
const [filterViewed, setFilterViewed] = useState<boolean>(false);
// 分类筛选核心逻辑(支持特殊分类)
const getCurrentCategoryEpisodes = () => {
if (activeCategory === '7') {
// 最喜爱(评分≥9.0)
return episodeList.filter(ep => ep.rating >= 9.0);
} else if (activeCategory === '8') {
// 未观看
return episodeList.filter(ep => !ep.viewed);
} else {
// 常规季数分类
const seasonNum = parseInt(categories.find(cat => cat.id === activeCategory)?.name.replace('季', '') || '1');
return episodeList.filter(ep => ep.season === seasonNum);
}
};
// 组合筛选+排序逻辑
const sortedEpisodes = getCurrentCategoryEpisodes()
// 多维度排序
.sort((a, b) => {
switch (sortBy) {
case 'number':
// 先按季数排序,再按集数排序
return a.season !== b.season ? a.season - b.season : a.episodeNumber - b.episodeNumber;
case 'date':
// 按播出日期排序
return new Date(a.airDate).getTime() - new Date(b.airDate).getTime();
case 'rating':
// 按评分降序排序
return b.rating - a.rating;
default:
return 0;
}
})
// 关键词搜索筛选(标题+描述)
.filter(episode =>
searchQuery === '' ||
episode.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
episode.description.toLowerCase().includes(searchQuery.toLowerCase())
);
筛选排序设计要点:
- 混合分类体系:支持常规的季数分类和特殊的业务分类(最喜爱、未观看);
- 复合排序逻辑:集数排序时先按季数再按集数,符合用户观看习惯;
- 多字段搜索:支持标题和描述的全文搜索,提升搜索体验;
- 降序评分排序:评分排序采用降序,符合用户优先看高分剧集的需求;
- 类型安全:排序字段使用联合类型限定,避免非法值输入。
(2)状态管理
// 切换观看状态(不可变更新)
const toggleViewed = (id: string) => {
setEpisodeList(episodeList.map(episode =>
episode.id === id ? { ...episode, viewed: !episode.viewed } : episode
));
};
// 评分星级转换(数据可视化)
const getRatingStars = (rating: number) => {
const stars = [];
const fullStars = Math.floor(rating / 2);
for (let i = 0; i < fullStars; i++) {
stars.push('★');
}
return stars.join('');
};
状态管理设计亮点:
- 不可变更新:使用数组
map和对象展开语法实现状态的不可变更新,符合 React 最佳实践; - 数据可视化转换:评分数值转换为星级展示,提升用户体验;
- 单一职责原则:每个函数只负责单一功能,便于维护和扩展;
- 无副作用设计:状态更新函数纯逻辑处理,无外部依赖。
(1)分类标签栏
实现了移动端经典的横向滚动分类标签栏,支持常规分类和特殊分类的统一展示:
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.categoriesContainer}
>
<View style={styles.categoriesList}>
{categories.map(category => (
<TouchableOpacity
key={category.id}
style={[
styles.categoryItem,
activeCategory === category.id && styles.activeCategory
]}
onPress={() => setActiveCategory(category.id)}
>
<Text style={[
styles.categoryIcon,
activeCategory === category.id && styles.activeCategoryIcon
]}>
{category.icon}
</Text>
<Text style={[
styles.categoryText,
activeCategory === category.id && styles.activeCategoryText
]}>
{category.name}
</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
(2)筛选与排序
<View style={styles.filtersContainer}>
{/* 观看状态筛选按钮 */}
<TouchableOpacity
style={[styles.filterButton, filterViewed && styles.activeFilter]}
onPress={() => setFilterViewed(!filterViewed)}
>
<Text style={styles.filterText}>只看已观看</Text>
</TouchableOpacity>
{/* 排序选项组 */}
<View style={styles.sortContainer}>
<Text style={styles.sortLabel}>排序:</Text>
<TouchableOpacity
style={[styles.sortButton, sortBy === 'number' && styles.activeSort]}
onPress={() => setSortBy('number')}
>
<Text style={styles.sortText}>编号</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.sortButton, sortBy === 'date' && styles.activeSort]}
onPress={() => setSortBy('date')}
>
<Text style={styles.sortText}>日期</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.sortButton, sortBy === 'rating' && styles.activeSort]}
onPress={() => setSortBy('rating')}
>
<Text style={styles.sortText}>评分</Text>
</TouchableOpacity>
</View>
</View>
(3)剧集列表
const renderEpisodeItem = ({ item }: { item: Episode }) => (
<View style={styles.episodeItem}>
{/* 图片占位区 */}
<View style={styles.imageContainer}>
<View style={styles.imagePlaceholder}>
<Text style={styles.imageText}>📺</Text>
</View>
</View>
{/* 信息区域 */}
<View style={styles.infoContainer}>
{/* 标题和观看状态 */}
<View style={styles.headerContainer}>
<Text style={styles.title}>
S{item.season}E{item.episodeNumber} - {item.title}
</Text>
<TouchableOpacity
style={styles.viewedButton}
onPress={() => toggleViewed(item.id)}
>
<Text style={styles.viewedText}>{item.viewed ? '✅' : '⬜'}</Text>
</TouchableOpacity>
</View>
{/* 基础信息 */}
<Text style={styles.airDate}>播出日期: {item.airDate}</Text>
<Text style={styles.duration}>时长: {item.duration}</Text>
<Text style={styles.description} numberOfLines={2}>{item.description}</Text>
{/* 评分和播放按钮 */}
<View style={styles.footerContainer}>
<View style={styles.ratingContainer}>
<Text style={styles.ratingStars}>{getRatingStars(item.rating)}</Text>
<Text style={styles.ratingValue}>{item.rating}/10</Text>
</View>
<TouchableOpacity
style={styles.watchButton}
onPress={() => Alert.alert('播放', `正在播放: ${item.title}`)}
>
<Text style={styles.watchButtonText}>▶️ 播放</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
将该 React Native 瑞克和莫蒂剧集库应用适配到鸿蒙平台,核心是将 React 的状态管理、多维度筛选排序、列表渲染等核心能力映射到鸿蒙 ArkTS + ArkUI 生态,以下是完整的适配方案。
1. 核心技术栈映射
| React Native 核心能力 | 鸿蒙 ArkTS 对应实现 | 适配关键说明 |
|---|---|---|
useState 状态管理 |
@State/@Link |
状态声明语法替换 |
FlatList 列表渲染 |
List + LazyForEach |
高性能列表渲染 |
ScrollView (横向) |
Scroll + scrollDirection |
横向滚动容器替换 |
TouchableOpacity |
Button + stateEffect(false) |
可点击组件替换 |
StyleSheet.create |
@Styles/@Extend + 内联样式 |
样式体系重构 |
Alert.alert |
AlertDialog 组件 |
弹窗交互替换 |
| 条件样式(数组语法) | 三元运算符 + 内联样式 | 样式条件判断适配 |
Dimensions 尺寸获取 |
@ohos.window API |
屏幕尺寸获取适配 |
numberOfLines 文本行数限制 |
maxLines 属性 |
文本展示适配 |
2. 鸿蒙端
// index.ets - 鸿蒙端瑞克和莫蒂剧集库完整实现
import router from '@ohos.router';
import window from '@ohos.window';
// 剧集类型定义(与RN端保持一致)
type Episode = {
id: string;
season: number;
episodeNumber: number;
title: string;
airDate: string;
description: string;
duration: string;
viewed: boolean;
rating: number;
};
// 分类类型定义(与RN端保持一致)
type Category = {
id: string;
name: string;
icon: string;
};
// 模拟数据(与RN端保持一致)
const categories: Category[] = [
{ id: '1', name: '第一季', icon: '1️⃣' },
{ id: '2', name: '第二季', icon: '2️⃣' },
{ id: '3', name: '第三季', icon: '3️⃣' },
{ id: '4', name: '第四季', icon: '4️⃣' },
{ id: '5', name: '第五季', icon: '5️⃣' },
{ id: '6', name: '特别篇', icon: '⭐' },
{ id: '7', name: '最喜爱', icon: '❤️' },
{ id: '8', name: '未观看', icon: '⏱️' },
];
const episodes: Episode[] = [
{
id: '1',
season: 1,
episodeNumber: 1,
title: 'Pilot',
airDate: '2013-12-02',
description: 'Rick带孙子Morty去外星球冒险,试图拯救他的女儿Summer。',
duration: '22:00',
viewed: true,
rating: 9.2
},
{
id: '2',
season: 1,
episodeNumber: 2,
title: 'Lawnmower Dog',
airDate: '2013-12-09',
description: 'Jerry试图让Summer参加夏令营,而Rick则帮助Morty报复学校的恶霸。',
duration: '22:00',
viewed: false,
rating: 8.8
},
{
id: '3',
season: 1,
episodeNumber: 3,
title: 'Anatomy Park',
airDate: '2013-12-16',
description: 'Rick和Morty在一个巨大的人体模型中冒险。',
duration: '22:00',
viewed: true,
rating: 9.0
},
{
id: '4',
season: 1,
episodeNumber: 4,
title: 'M. Night Shaym-Aliens!',
airDate: '2014-01-13',
description: 'Rick被外星人绑架,Morty必须独自拯救他。',
duration: '22:00',
viewed: false,
rating: 9.1
},
{
id: '5',
season: 2,
episodeNumber: 1,
title: 'A Rickle in Time',
airDate: '2015-07-26',
description: 'Rick从冷冻状态中醒来,发现宇宙已经分裂成多个时间线。',
duration: '22:00',
viewed: true,
rating: 9.3
},
{
id: '6',
season: 2,
episodeNumber: 2,
title: 'Mortynight Run',
airDate: '2015-08-02',
description: 'Morty和Rick在银河系中逃避赏金猎人。',
duration: '22:00',
viewed: true,
rating: 9.4
},
{
id: '7',
season: 3,
episodeNumber: 1,
title: 'The Rickshank Rickdemption',
airDate: '2017-04-01',
description: 'Rick从监狱中逃脱,重新加入家庭生活。',
duration: '22:00',
viewed: false,
rating: 9.5
},
{
id: '8',
season: 3,
episodeNumber: 2,
title: 'Rickmancing the Stone',
airDate: '2017-07-30',
description: 'Rick、Morty和Summer被困在后启示录世界。',
duration: '22:00',
viewed: false,
rating: 8.9
},
{
id: '9',
season: 4,
episodeNumber: 1,
title: 'Edge of Tomorty: Rick Die Rickpeat',
airDate: '2019-11-10',
description: 'Rick和Morty在农场遭遇危险生物。',
duration: '22:00',
viewed: true,
rating: 8.7
},
{
id: '10',
season: 5,
episodeNumber: 1,
title: 'Mort Dinner Rick Andre',
airDate: '2021-06-20',
description: 'Rick邀请全家人与他的外星朋友共进晚餐。',
duration: '22:00',
viewed: false,
rating: 9.0
},
];
@Entry
@Component
struct RickAndMortyApp {
// 核心状态管理(对应RN的useState)
@State episodeList: Episode[] = episodes;
@State activeCategory: string = '1';
@State searchQuery: string = '';
@State sortBy: 'number' | 'date' | 'rating' = 'number';
@State filterViewed: boolean = false;
// 屏幕尺寸(对应RN的Dimensions)
@State windowWidth: number = 0;
@State windowHeight: number = 0;
// 通用样式封装
@Styles
cardShadow() {
.shadow({ radius: 2, color: '#000', opacity: 0.1, offsetX: 0, offsetY: 1 });
}
// 组件生命周期(获取屏幕尺寸)
async aboutToAppear() {
const windowClass = await window.getLastWindow(getContext());
const windowSize = await windowClass.getWindowProperties();
this.windowWidth = windowSize.windowRect.width;
this.windowHeight = windowSize.windowRect.height;
}
// 获取当前分类的剧集(逻辑与RN完全一致)
private getCurrentCategoryEpisodes(): Episode[] {
if (this.activeCategory === '7') {
// 最喜爱
return this.episodeList.filter(ep => ep.rating >= 9.0);
} else if (this.activeCategory === '8') {
// 未观看
return this.episodeList.filter(ep => !ep.viewed);
} else {
const seasonNum = parseInt(categories.find(cat => cat.id === this.activeCategory)?.name.replace('季', '') || '1');
return this.episodeList.filter(ep => ep.season === seasonNum);
}
}
// 按排序方式整理数据(逻辑与RN完全一致)
private get sortedEpisodes(): Episode[] {
return this.getCurrentCategoryEpisodes()
.sort((a, b) => {
switch (this.sortBy) {
case 'number':
return a.season !== b.season ? a.season - b.season : a.episodeNumber - b.episodeNumber;
case 'date':
return new Date(a.airDate).getTime() - new Date(b.airDate).getTime();
case 'rating':
return b.rating - a.rating;
default:
return 0;
}
})
.filter(episode =>
this.searchQuery === '' ||
episode.title.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
episode.description.toLowerCase().includes(this.searchQuery.toLowerCase())
);
}
// 切换观看状态(逻辑与RN完全一致)
private toggleViewed(id: string) {
this.episodeList = this.episodeList.map(episode =>
episode.id === id ? { ...episode, viewed: !episode.viewed } : episode
);
}
// 获取评分星级(逻辑与RN完全一致)
private getRatingStars(rating: number): string {
const stars = [];
const fullStars = Math.floor(rating / 2);
for (let i = 0; i < fullStars; i++) {
stars.push('★');
}
return stars.join('');
}
// 渲染剧集项(Builder函数替换RN的renderEpisodeItem)
@Builder
renderEpisodeItem(item: Episode) {
Row({ space: 12 }) {
// 图片占位区
Column() {
Stack() {
Text('📺')
.fontSize(24)
.textAlign(TextAlign.Center);
}
.width(80)
.height(60)
.backgroundColor('#e2e8f0')
.borderRadius(8)
.justifyContent(FlexAlign.Center);
}
// 信息区域
Column({ space: 4 }) {
// 标题和观看状态
Row({ space: 0 }) {
Text(`S${item.season}E${item.episodeNumber} - ${item.title}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
.flex(1);
Button(item.viewed ? '✅' : '⬜')
.backgroundColor(Color.Transparent)
.fontSize(18)
.stateEffect(true)
.marginLeft(8)
.onClick(() => this.toggleViewed(item.id));
}
// 播出日期
Text(`播出日期: ${item.airDate}`)
.fontSize(12)
.fontColor('#64748b');
// 时长
Text(`时长: ${item.duration}`)
.fontSize(12)
.fontColor('#64748b');
// 剧情描述(限制行数)
Text(item.description)
.fontSize(14)
.fontColor('#64748b')
.maxLines(2);
// 评分和播放按钮
Row({ space: 0 }) {
Row({ space: 4 }) {
Text(this.getRatingStars(item.rating))
.fontSize(16)
.fontColor('#f59e0b');
Text(`${item.rating}/10`)
.fontSize(12)
.fontColor('#64748b');
}
Button('▶️ 播放')
.backgroundColor('#3b82f6')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(6)
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor('#ffffff')
.stateEffect(true)
.marginLeft('auto')
.onClick(() => {
AlertDialog.show({
title: '播放',
message: `正在播放: ${item.title}`,
confirm: { value: '确定' }
});
});
}
}
.flex(1);
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(12)
.marginBottom(12)
.cardShadow();
}
// 空状态渲染
@Builder
renderEmptyState() {
Column({ space: 8 }) {
Text('暂无剧集数据')
.fontSize(16)
.fontColor('#64748b');
Text('请调整筛选条件查看')
.fontSize(14)
.fontColor('#94a3b8')
.textAlign(TextAlign.Center);
}
.width('100%')
.paddingVertical(60)
.alignItems(ItemAlign.Center);
}
build() {
Column({ space: 0 }) {
// 头部组件
this.Header();
// 内容区域
Scroll() {
Column({ space: 16 }) {
// 搜索栏
this.SearchBar();
// 分类标签
this.CategoryTabs();
// 筛选和排序选项
this.FiltersAndSort();
// 统计信息
this.StatsCard();
// 剧集列表标题
Text(`${categories.find(c => c.id === this.activeCategory)?.name || '剧集'} (${this.sortedEpisodes.length})`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
.marginVertical(12)
.width('100%');
// 剧集列表
if (this.sortedEpisodes.length > 0) {
List() {
LazyForEach(new EpisodeDataSource(this.sortedEpisodes), (item: Episode) => {
ListItem() {
this.renderEpisodeItem(item);
}
});
}
.width('100%')
.scrollBar(BarState.Off);
} else {
this.renderEmptyState();
}
// 介绍说明
this.InfoCard();
}
.padding(16)
.width('100%');
}
.flex(1)
.width('100%');
// 底部导航
this.BottomNav();
}
.width('100%')
.height('100%')
.backgroundColor('#f8fafc')
.safeArea(true);
}
// 头部组件
@Builder
Header() {
Row({ space: 0 }) {
Text('瑞克和莫蒂')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Row({ space: 12 }) {
// 搜索按钮
Button()
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('#f1f5f9')
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '搜索',
confirm: { value: '确定' }
});
}) {
Text('🔍')
.fontSize(18)
.fontColor('#64748b');
}
// 信息按钮
Button()
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('#f1f5f9')
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '关于',
message: '瑞克和莫蒂官方剧集指南',
confirm: { value: '确定' }
});
}) {
Text('ℹ️')
.fontSize(18)
.fontColor('#64748b');
}
}
.marginLeft('auto');
}
.padding(20)
.backgroundColor('#ffffff')
.borderBottom({ width: 1, color: '#e2e8f0' })
.width('100%');
}
// 搜索栏
@Builder
SearchBar() {
Row({ space: 12 }) {
Text('🔍')
.fontSize(18)
.fontColor('#64748b');
Text('搜索剧集标题或描述')
.fontSize(14)
.fontColor('#94a3b8')
.flex(1);
}
.backgroundColor('#ffffff')
.borderRadius(20)
.padding({ top: 12, bottom: 12, left: 16, right: 16 })
.cardShadow()
.width('100%');
}
// 分类标签栏
@Builder
CategoryTabs() {
Column({ space: 0 }) {
Text('剧集分类')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
.marginVertical(12)
.width('100%');
Scroll({ scrollDirection: ScrollDirection.Horizontal }) {
Row({ space: 12 }) {
ForEach(categories, (category: Category) => {
Button()
.backgroundColor(this.activeCategory === category.id ? '#3b82f6' : '#ffffff')
.borderRadius(20)
.padding({ top: 8, bottom: 8, left: 16, right: 16 })
.stateEffect(true)
.cardShadow()
.onClick(() => this.activeCategory = category.id) {
Column({ space: 4 }) {
Text(category.icon)
.fontSize(16)
.fontColor(this.activeCategory === category.id ? '#ffffff' : '#64748b');
Text(category.name)
.fontSize(12)
.fontColor(this.activeCategory === category.id ? '#ffffff' : '#64748b')
.fontWeight(this.activeCategory === category.id ? FontWeight.Medium : FontWeight.Normal);
}
};
});
}
.width('auto');
}
.scrollBar(BarState.Off);
}
.width('100%');
}
// 筛选和排序选项
@Builder
FiltersAndSort() {
Column({ space: 12 }) {
// 观看状态筛选
Button('只看已观看')
.backgroundColor(this.filterViewed ? '#3b82f6' : '#f1f5f9')
.borderRadius(20)
.padding({ top: 6, bottom: 6, left: 12, right: 12 })
.fontSize(12)
.fontColor('#3b82f6')
.fontWeight(FontWeight.Medium)
.stateEffect(true)
.alignSelf(ItemAlign.Start)
.onClick(() => this.filterViewed = !this.filterViewed);
// 排序选项
Row({ space: 8 }) {
Text('排序:')
.fontSize(14)
.fontColor('#64748b');
// 编号排序
Button('编号')
.backgroundColor(this.sortBy === 'number' ? '#3b82f6' : '#f1f5f9')
.borderRadius(20)
.padding({ top: 6, bottom: 6, left: 12, right: 12 })
.fontSize(12)
.fontColor('#3b82f6')
.stateEffect(true)
.onClick(() => this.sortBy = 'number');
// 日期排序
Button('日期')
.backgroundColor(this.sortBy === 'date' ? '#3b82f6' : '#f1f5f9')
.borderRadius(20)
.padding({ top: 6, bottom: 6, left: 12, right: 12 })
.fontSize(12)
.fontColor('#3b82f6')
.stateEffect(true)
.onClick(() => this.sortBy = 'date');
// 评分排序
Button('评分')
.backgroundColor(this.sortBy === 'rating' ? '#3b82f6' : '#f1f5f9')
.borderRadius(20)
.padding({ top: 6, bottom: 6, left: 12, right: 12 })
.fontSize(12)
.fontColor('#3b82f6')
.stateEffect(true)
.onClick(() => this.sortBy = 'rating');
}
.width('100%')
.justifyContent(FlexAlign.Start);
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(12)
.cardShadow()
.width('100%');
}
// 统计信息卡片
@Builder
StatsCard() {
Row({ space: 8 }) {
// 当前分类
Column({ space: 4 }) {
Text(`${this.sortedEpisodes.length}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#3b82f6');
Text('当前分类')
.fontSize(12)
.fontColor('#64748b');
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(12)
.alignItems(ItemAlign.Center)
.flex(1)
.cardShadow();
// 已观看
Column({ space: 4 }) {
Text(`${this.episodeList.filter(e => e.viewed).length}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#3b82f6');
Text('已观看')
.fontSize(12)
.fontColor('#64748b');
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(12)
.alignItems(ItemAlign.Center)
.flex(1)
.cardShadow();
// 未观看
Column({ space: 4 }) {
Text(`${this.episodeList.filter(e => !e.viewed).length}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#3b82f6');
Text('未观看')
.fontSize(12)
.fontColor('#64748b');
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(12)
.alignItems(ItemAlign.Center)
.flex(1)
.cardShadow();
// 季数
Column({ space: 4 }) {
Text('5')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#3b82f6');
Text('季数')
.fontSize(12)
.fontColor('#64748b');
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(12)
.alignItems(ItemAlign.Center)
.flex(1)
.cardShadow();
}
.width('100%');
}
// 使用说明卡片
@Builder
InfoCard() {
Column({ space: 8 }) {
Text('剧集说明')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Text('• 点击✅/⬜按钮标记观看状态')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
Text('• 点击播放按钮观看剧集')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
Text('• 可按季度、观看状态筛选')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
Text('• 支持按编号、日期、评分排序')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(16)
.cardShadow()
.width('100%');
}
// 底部导航
@Builder
BottomNav() {
Row({ space: 0 }) {
// 首页
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => this.activeCategory = '1') {
Column({ space: 4 }) {
Text('🏠')
.fontSize(20)
.fontColor('#94a3b8');
Text('首页')
.fontSize(12)
.fontColor('#94a3b8');
}
};
// 分类
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '分类',
confirm: { value: '确定' }
});
}) {
Column({ space: 4 }) {
Text('📚')
.fontSize(20)
.fontColor('#94a3b8');
Text('分类')
.fontSize(12)
.fontColor('#94a3b8');
}
};
// 观看历史
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '观看历史',
confirm: { value: '确定' }
});
}) {
Column({ space: 4 }) {
Text('🕒')
.fontSize(20)
.fontColor('#94a3b8');
Text('历史')
.fontSize(12)
.fontColor('#94a3b8');
}
};
// 我的
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '我的',
confirm: { value: '确定' }
});
}) {
Column({ space: 4 }) {
Text('👤')
.fontSize(20)
.fontColor('#94a3b8');
Text('我的')
.fontSize(12)
.fontColor('#94a3b8');
}
};
}
.backgroundColor('#ffffff')
.borderTop({ width: 1, color: '#e2e8f0' })
.paddingVertical(12)
.width('100%');
}
}
// 鸿蒙List数据源(LazyForEach必需)
class EpisodeDataSource implements IDataSource {
private data: Episode[];
private listener: DataChangeListener | null = null;
constructor(data: Episode[]) {
this.data = data;
}
totalCount(): number {
return this.data.length;
}
getData(index: number): Episode {
return this.data[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
unregisterDataChangeListener(): void {
this.listener = null;
}
}
该瑞克和莫蒂剧集库应用的跨端适配实践验证了影视类列表应用从 React Native 向鸿蒙迁移的高效性,核心的多维度筛选和复合排序逻辑可实现完全复用,仅需适配UI组件层和状态管理语法,这种适配模式特别适合数据驱动的影视类应用开发,能够显著提升跨端开发效率。
真实演示案例代码:
// app.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, FlatList } from 'react-native';
// Base64 图标库
const ICONS_BASE64 = {
home: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
episode: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
filter: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
sort: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
watch: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
info: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
more: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};
const { width, height } = Dimensions.get('window');
// 剧集类型
type Episode = {
id: string;
season: number;
episodeNumber: number;
title: string;
airDate: string;
description: string;
duration: string;
viewed: boolean;
rating: number;
};
// 分类类型
type Category = {
id: string;
name: string;
icon: string;
};
// 模拟数据
const categories: Category[] = [
{ id: '1', name: '第一季', icon: '1️⃣' },
{ id: '2', name: '第二季', icon: '2️⃣' },
{ id: '3', name: '第三季', icon: '3️⃣' },
{ id: '4', name: '第四季', icon: '4️⃣' },
{ id: '5', name: '第五季', icon: '5️⃣' },
{ id: '6', name: '特别篇', icon: '⭐' },
{ id: '7', name: '最喜爱', icon: '❤️' },
{ id: '8', name: '未观看', icon: '⏱️' },
];
const episodes: Episode[] = [
{
id: '1',
season: 1,
episodeNumber: 1,
title: 'Pilot',
airDate: '2013-12-02',
description: 'Rick带孙子Morty去外星球冒险,试图拯救他的女儿Summer。',
duration: '22:00',
viewed: true,
rating: 9.2
},
{
id: '2',
season: 1,
episodeNumber: 2,
title: 'Lawnmower Dog',
airDate: '2013-12-09',
description: 'Jerry试图让Summer参加夏令营,而Rick则帮助Morty报复学校的恶霸。',
duration: '22:00',
viewed: false,
rating: 8.8
},
{
id: '3',
season: 1,
episodeNumber: 3,
title: 'Anatomy Park',
airDate: '2013-12-16',
description: 'Rick和Morty在一个巨大的人体模型中冒险。',
duration: '22:00',
viewed: true,
rating: 9.0
},
{
id: '4',
season: 1,
episodeNumber: 4,
title: 'M. Night Shaym-Aliens!',
airDate: '2014-01-13',
description: 'Rick被外星人绑架,Morty必须独自拯救他。',
duration: '22:00',
viewed: false,
rating: 9.1
},
{
id: '5',
season: 2,
episodeNumber: 1,
title: 'A Rickle in Time',
airDate: '2015-07-26',
description: 'Rick从冷冻状态中醒来,发现宇宙已经分裂成多个时间线。',
duration: '22:00',
viewed: true,
rating: 9.3
},
{
id: '6',
season: 2,
episodeNumber: 2,
title: 'Mortynight Run',
airDate: '2015-08-02',
description: 'Morty和Rick在银河系中逃避赏金猎人。',
duration: '22:00',
viewed: true,
rating: 9.4
},
{
id: '7',
season: 3,
episodeNumber: 1,
title: 'The Rickshank Rickdemption',
airDate: '2017-04-01',
description: 'Rick从监狱中逃脱,重新加入家庭生活。',
duration: '22:00',
viewed: false,
rating: 9.5
},
{
id: '8',
season: 3,
episodeNumber: 2,
title: 'Rickmancing the Stone',
airDate: '2017-07-30',
description: 'Rick、Morty和Summer被困在后启示录世界。',
duration: '22:00',
viewed: false,
rating: 8.9
},
{
id: '9',
season: 4,
episodeNumber: 1,
title: 'Edge of Tomorty: Rick Die Rickpeat',
airDate: '2019-11-10',
description: 'Rick和Morty在农场遭遇危险生物。',
duration: '22:00',
viewed: true,
rating: 8.7
},
{
id: '10',
season: 5,
episodeNumber: 1,
title: 'Mort Dinner Rick Andre',
airDate: '2021-06-20',
description: 'Rick邀请全家人与他的外星朋友共进晚餐。',
duration: '22:00',
viewed: false,
rating: 9.0
},
];
const RickAndMortyApp: React.FC = () => {
const [episodeList, setEpisodeList] = useState<Episode[]>(episodes);
const [activeCategory, setActiveCategory] = useState<string>('1');
const [searchQuery, setSearchQuery] = useState<string>('');
const [sortBy, setSortBy] = useState<'number' | 'date' | 'rating'>('number');
const [filterViewed, setFilterViewed] = useState<boolean>(false);
// 获取当前分类的剧集
const getCurrentCategoryEpisodes = () => {
if (activeCategory === '7') {
// 最喜爱
return episodeList.filter(ep => ep.rating >= 9.0);
} else if (activeCategory === '8') {
// 未观看
return episodeList.filter(ep => !ep.viewed);
} else {
const seasonNum = parseInt(categories.find(cat => cat.id === activeCategory)?.name.replace('季', '') || '1');
return episodeList.filter(ep => ep.season === seasonNum);
}
};
// 按排序方式整理数据
const sortedEpisodes = getCurrentCategoryEpisodes().sort((a, b) => {
switch (sortBy) {
case 'number':
return a.season !== b.season ? a.season - b.season : a.episodeNumber - b.episodeNumber;
case 'date':
return new Date(a.airDate).getTime() - new Date(b.airDate).getTime();
case 'rating':
return b.rating - a.rating;
default:
return 0;
}
}).filter(episode =>
searchQuery === '' ||
episode.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
episode.description.toLowerCase().includes(searchQuery.toLowerCase())
);
// 切换观看状态
const toggleViewed = (id: string) => {
setEpisodeList(episodeList.map(episode =>
episode.id === id ? { ...episode, viewed: !episode.viewed } : episode
));
};
// 获取评分星级
const getRatingStars = (rating: number) => {
const stars = [];
const fullStars = Math.floor(rating / 2);
for (let i = 0; i < fullStars; i++) {
stars.push('★');
}
return stars.join('');
};
// 渲染剧集项
const renderEpisodeItem = ({ item }: { item: Episode }) => (
<View style={styles.episodeItem}>
<View style={styles.imageContainer}>
<View style={styles.imagePlaceholder}>
<Text style={styles.imageText}>📺</Text>
</div>
</View>
<View style={styles.infoContainer}>
<View style={styles.headerContainer}>
<Text style={styles.title}>
S{item.season}E{item.episodeNumber} - {item.title}
</Text>
<TouchableOpacity
style={styles.viewedButton}
onPress={() => toggleViewed(item.id)}
>
<Text style={styles.viewedText}>{item.viewed ? '✅' : '⬜'}</Text>
</TouchableOpacity>
</View>
<Text style={styles.airDate}>播出日期: {item.airDate}</Text>
<Text style={styles.duration}>时长: {item.duration}</Text>
<Text style={styles.description} numberOfLines={2}>{item.description}</Text>
<View style={styles.footerContainer}>
<View style={styles.ratingContainer}>
<Text style={styles.ratingStars}>{getRatingStars(item.rating)}</Text>
<Text style={styles.ratingValue}>{item.rating}/10</Text>
</div>
<TouchableOpacity
style={styles.watchButton}
onPress={() => Alert.alert('播放', `正在播放: ${item.title}`)}
>
<Text style={styles.watchButtonText}>▶️ 播放</Text>
</TouchableOpacity>
</div>
</View>
</View>
);
return (
<SafeAreaView style={styles.container}>
{/* 头部 */}
<View style={styles.header}>
<Text style={styles.title}>瑞克和莫蒂</Text>
<View style={styles.headerActions}>
<TouchableOpacity style={styles.searchButton} onPress={() => Alert.alert('搜索')}>
<Text style={styles.searchIcon}>🔍</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.infoButton} onPress={() => Alert.alert('关于', '瑞克和莫蒂官方剧集指南')}>
<Text style={styles.infoIcon}>ℹ️</Text>
</TouchableOpacity>
</div>
</View>
<ScrollView style={styles.content}>
{/* 搜索栏 */}
<View style={styles.searchContainer}>
<Text style={styles.searchIcon}>🔍</Text>
<Text style={styles.searchPlaceholder}>搜索剧集标题或描述</Text>
</View>
{/* 分类标签 */}
<Text style={styles.sectionTitle}>剧集分类</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.categoriesContainer}
>
<View style={styles.categoriesList}>
{categories.map(category => (
<TouchableOpacity
key={category.id}
style={[
styles.categoryItem,
activeCategory === category.id && styles.activeCategory
]}
onPress={() => setActiveCategory(category.id)}
>
<Text style={[
styles.categoryIcon,
activeCategory === category.id && styles.activeCategoryIcon
]}>
{category.icon}
</Text>
<Text style={[
styles.categoryText,
activeCategory === category.id && styles.activeCategoryText
]}>
{category.name}
</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
{/* 筛选和排序选项 */}
<View style={styles.filtersContainer}>
<TouchableOpacity
style={[styles.filterButton, filterViewed && styles.activeFilter]}
onPress={() => setFilterViewed(!filterViewed)}
>
<Text style={styles.filterText}>只看已观看</Text>
</TouchableOpacity>
<View style={styles.sortContainer}>
<Text style={styles.sortLabel}>排序:</Text>
<TouchableOpacity
style={[styles.sortButton, sortBy === 'number' && styles.activeSort]}
onPress={() => setSortBy('number')}
>
<Text style={styles.sortText}>编号</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.sortButton, sortBy === 'date' && styles.activeSort]}
onPress={() => setSortBy('date')}
>
<Text style={styles.sortText}>日期</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.sortButton, sortBy === 'rating' && styles.activeSort]}
onPress={() => setSortBy('rating')}
>
<Text style={styles.sortText}>评分</Text>
</TouchableOpacity>
</View>
</View>
{/* 统计信息 */}
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{sortedEpisodes.length}</Text>
<Text style={styles.statLabel}>当前分类</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{episodeList.filter(e => e.viewed).length}</Text>
<Text style={styles.statLabel}>已观看</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{episodeList.filter(e => !e.viewed).length}</Text>
<Text style={styles.statLabel}>未观看</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>5</Text>
<Text style={styles.statLabel}>季数</Text>
</View>
</View>
{/* 剧集列表 */}
<Text style={styles.sectionTitle}>
{categories.find(c => c.id === activeCategory)?.name || '剧集'} ({sortedEpisodes.length})
</Text>
<FlatList
data={sortedEpisodes}
renderItem={renderEpisodeItem}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
/>
{/* 介绍说明 */}
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>剧集说明</Text>
<Text style={styles.infoText}>• 点击✅/⬜按钮标记观看状态</Text>
<Text style={styles.infoText}>• 点击播放按钮观看剧集</Text>
<Text style={styles.infoText}>• 可按季度、观看状态筛选</Text>
<Text style={styles.infoText}>• 支持按编号、日期、评分排序</Text>
</View>
</ScrollView>
{/* 底部导航 */}
<View style={styles.bottomNav}>
<TouchableOpacity
style={styles.navItem}
onPress={() => setActiveCategory('1')}
>
<Text style={styles.navIcon}>🏠</Text>
<Text style={styles.navText}>首页</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('分类')}
>
<Text style={styles.navIcon}>📚</Text>
<Text style={styles.navText}>分类</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('观看历史')}
>
<Text style={styles.navIcon}>🕒</Text>
<Text style={styles.navText}>历史</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('我的')}
>
<Text style={styles.navIcon}>👤</Text>
<Text style={styles.navText}>我的</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
backgroundColor: '#ffffff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#1e293b',
},
headerActions: {
flexDirection: 'row',
},
searchButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#f1f5f9',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
searchIcon: {
fontSize: 18,
color: '#64748b',
},
infoButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#f1f5f9',
alignItems: 'center',
justifyContent: 'center',
},
infoIcon: {
fontSize: 18,
color: '#64748b',
},
content: {
flex: 1,
padding: 16,
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#ffffff',
borderRadius: 20,
paddingVertical: 12,
paddingHorizontal: 16,
marginBottom: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
searchPlaceholder: {
fontSize: 14,
color: '#94a3b8',
marginLeft: 12,
flex: 1,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
marginVertical: 12,
},
categoriesContainer: {
marginBottom: 16,
},
categoriesList: {
flexDirection: 'row',
},
categoryItem: {
backgroundColor: '#ffffff',
borderRadius: 20,
paddingVertical: 8,
paddingHorizontal: 16,
marginRight: 12,
alignItems: 'center',
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
activeCategory: {
backgroundColor: '#3b82f6',
},
categoryIcon: {
fontSize: 16,
marginBottom: 4,
color: '#64748b',
},
activeCategoryIcon: {
color: '#ffffff',
},
categoryText: {
fontSize: 12,
color: '#64748b',
},
activeCategoryText: {
color: '#ffffff',
fontWeight: '500',
},
filtersContainer: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 12,
marginBottom: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
filterButton: {
backgroundColor: '#f1f5f9',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 20,
alignSelf: 'flex-start',
marginBottom: 12,
},
activeFilter: {
backgroundColor: '#3b82f6',
},
filterText: {
fontSize: 12,
color: '#3b82f6',
fontWeight: '500',
},
activeFilterText: {
color: '#ffffff',
},
sortContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
sortLabel: {
fontSize: 14,
color: '#64748b',
marginRight: 8,
},
sortButton: {
backgroundColor: '#f1f5f9',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 20,
marginLeft: 8,
},
activeSort: {
backgroundColor: '#3b82f6',
},
sortText: {
fontSize: 12,
color: '#3b82f6',
},
activeSortText: {
color: '#ffffff',
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
},
statItem: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 12,
alignItems: 'center',
flex: 1,
marginHorizontal: 4,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
statNumber: {
fontSize: 18,
fontWeight: 'bold',
color: '#3b82f6',
},
statLabel: {
fontSize: 12,
color: '#64748b',
marginTop: 4,
},
episodeItem: {
backgroundColor: '#ffffff',
borderRadius: 12,
flexDirection: 'row',
padding: 12,
marginBottom: 12,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
imageContainer: {
marginRight: 12,
},
imagePlaceholder: {
width: 80,
height: 60,
backgroundColor: '#e2e8f0',
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
imageText: {
fontSize: 24,
},
infoContainer: {
flex: 1,
justifyContent: 'space-between',
},
headerContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
title: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
flex: 1,
},
viewedButton: {
marginLeft: 8,
},
viewedText: {
fontSize: 18,
},
airDate: {
fontSize: 12,
color: '#64748b',
marginBottom: 2,
},
duration: {
fontSize: 12,
color: '#64748b',
marginBottom: 4,
},
description: {
fontSize: 14,
color: '#64748b',
marginBottom: 8,
},
footerContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
ratingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
ratingStars: {
fontSize: 16,
color: '#f59e0b',
marginRight: 4,
},
ratingValue: {
fontSize: 12,
color: '#64748b',
},
watchButton: {
backgroundColor: '#3b82f6',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
},
watchButtonText: {
color: '#ffffff',
fontSize: 12,
fontWeight: '500',
},
infoCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginTop: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
infoTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 12,
},
infoText: {
fontSize: 14,
color: '#64748b',
lineHeight: 22,
marginBottom: 8,
},
bottomNav: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
paddingVertical: 12,
},
navItem: {
alignItems: 'center',
flex: 1,
},
navIcon: {
fontSize: 20,
color: '#94a3b8',
marginBottom: 4,
},
activeNavIcon: {
color: '#3b82f6',
},
navText: {
fontSize: 12,
color: '#94a3b8',
},
activeNavText: {
color: '#3b82f6',
fontWeight: '500',
},
});
export default RickAndMortyApp;

打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

本文探讨了一个基于React Native的剧集管理应用实现,重点分析了其架构设计、状态管理和数据处理策略。应用采用组件化架构,包含主应用组件、剧集列表、分类筛选、搜索排序等功能模块,实现了清晰的功能分离。使用useState管理剧集列表、分类、搜索等状态,并通过TypeScript确保类型安全。应用支持多种分类方式、多维排序和内容搜索功能,数据组织合理,界面布局层次分明。文章还提出了性能优化建议,如使用FlatList优化渲染效率,以及采用useReducer管理复杂状态。整体实现展现了React Native在媒体内容管理应用开发中的良好实践。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐




所有评论(0)