在移动应用开发中,媒体内容管理应用是一种常见的应用类型,需要考虑内容展示、分类筛选、搜索排序等多个方面。本文将深入分析一个功能完备的 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 定义了两个核心数据类型:

  1. Episode - 剧集类型,包含剧集的完整信息,如 ID、季、集数、标题、播出日期、描述、时长、观看状态和评分
  2. 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 布尔字段明确标识观看状态,支持用户交互;
  • 分级体系合理:通过 seasonepisodeNumber 实现剧集的层级管理;
  • 分类扩展性强:独立的 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在媒体内容管理应用开发中的良好实践。

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐