在React Native中开发一个旅行记录应用中的“收藏旅行”页面,你可以按照以下步骤进行:

  1. 环境准备

确保你已经安装了Node.js和React Native的开发环境。你可以使用npx react-native init TravelApp来创建一个新的React Native项目。

  1. 安装依赖

你可能需要一些额外的库来帮助你管理状态和进行网络请求。例如,使用@react-navigation/native@react-navigation/native-stack进行导航,使用react-native-vector-icons来添加图标,使用react-native-async-storage来保存用户数据等。

npm install @react-navigation/native @react-navigation/native-stack react-native-vector-icons react-native-async-storage
  1. 设置导航

在你的项目中设置导航。首先,安装依赖后,你可以在App.js中设置基本的导航结构。

import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from './screens/HomeScreen';
import FavoriteScreen from './screens/FavoriteScreen';

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Favorites" component={FavoriteScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;
  1. 创建收藏页面

screens文件夹中创建一个新的FavoriteScreen.js文件。这个页面将显示用户的收藏旅行记录。

import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Icon from 'react-native-vector-icons/FontAwesome'; // 假设你使用的是FontAwesome图标库

const FavoriteScreen = ({ navigation }) => {
  const [favorites, setFavorites] = useState([]);

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

  const loadFavorites = async () => {
    try {
      const data = await AsyncStorage.getItem('favorites');
      if (data !== null) {
        setFavorites(JSON.parse(data));
      }
    } catch (e) {
      console.error(e);
    }
  };

  return (
    <View style={styles.container}>
      <FlatList 
        data={favorites} 
        keyExtractor={item => item.id.toString()} 
        renderItem={({ item }) => (
          <TouchableOpacity style={styles.item} onPress={() => navigation.navigate('Details', { item })}>
            <Text>{item.title}</Text>
            <Icon name="heart" size={20} color="red" /> {/* 显示收藏图标 */}
          </TouchableOpacity>
        )} 
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20 },
  item: { padding: 10, marginVertical: 8, backgroundColor: 'f9c2ff', borderRadius: 5 },
});

export default FavoriteScreen;
  1. 添加收藏功能到其他页面(例如HomeScreen)

在你的主页面(例如HomeScreen)中,添加一个按钮允许用户将旅行添加到收藏中。这通常涉及到更新favorites数组并保存到AsyncStorage。例如:

const addToFavorites = async (item) => {
  try {
    let favorites = await AsyncStorage.getItem('favorites'); // 获取当前收藏数据
    favorites = favorites ? JSON.parse(favorites) : []; // 如果不存在,初始化数组
    favorites.push(item); // 添加新项目到收藏数组中
    await AsyncStorage.setItem('favorites', JSON.stringify(favorites)); // 保存更新后的收藏数据到AsyncStorage中
  } catch (e

真实实际案例代码演示:

// app.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal, Alert } from 'react-native';

// Base64 图标库
const ICONS = {
  mountain: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0zIDEybDE4LTEydi0yLjI0bC0xOC43NiAxMi41N3oiLz48cGF0aCBkPSJNMyAxMmw5LTZsOSA2diEyLjI0bC05LTYuMzEtOSA2LjMxdi0yLjI0eiIvPjxwYXRoIGQ9Ik0zIDEybDktNiA5IDZ2Mi4yNGwtOS02LjMxLTktNi4zMS0uNzYtLjUxdjIuMjRsLjczLjQ5eiIvPjwvc3ZnPg==',
  beach: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyYy00Ljk3IDAtOSA0LjAzLTkgOXM0LjAzIDkgOSA5IDktNC4wMyA5LTljMCA0Ljk3LTQuMDMtOS05LTl6Ii8+PHBhdGggZD0iTTEyIDRjMy44NyAwIDcgMy4xMyA3IDdzLTMuMTMgNy03IDctNy0zLjEzLTctNyAzLjEzLTcgNy03eiIvPjxwYXRoIGQ9Ik0xMiA2YzIuNzYgMCA1IDIuMjQgNSA1cy0yLjI0IDUtNSA1LTUtMi4yNC01LTUgMi4yNC01IDUtNXoiLz48L3N2Zz4=',
  city: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMCAyMGgtNnYtNmg0djZ6bS00LTR2NGg0di00aC00em04IDRoLTR2LTRoNHY0em0wLTR2LTRoLTR2NGg0em00IDRoLTR2LTRoNHY0em0wLTR2LTRoLTR2NGg0em0tOCA4aC00di00aDR2NHptLTQtNHY0aDR2LTRoLTR6bTggNGgtNHYtNGg0djR6bTAtNHYtNGgtNHY0aDR6bTQgNGgtNHYtNGg0djR6bTAtNHYtNGgtNHY0aDR6Ii8+PC9zdmc+',
  forest: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoLTJ2LTJoLTJ2LTJoLTJ2LTJoLTJ2LTJoLTJ2LTJoLTJ2LTJoLTJ2LTJoLTJ2LTJoLTJ2LTJoLTJ2LTJoLTJ2LTJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ2MmgydjJoMnYyaDJ......',
  heart: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyMS4zNWwtLjYxLS4yNy0yLjIxLS45OWMtLjM5LS4xOC0uNzEtLjQ5LS45MS0uODhsLS44OS0xLjg5Yy0uMTktLjM5LS40OS0uNzEtLjg4LS45MWwtMS44OS0uODljLS4zOS0uMTktLjcxLS40OS0uODgtLjg4bC0uOTktMi4yMWMtLjE4LS4zOS0uMjctLjYxLS4yNy0uNjFsLjI3LS42MS45OS0yLjIxYy4xOC0uMzkuNDktLjcxLjg4LS45MWwuODktMS44OWMuMTktLjM5LjQ5LS43MS44OC0uOTFsMi4yMS0uOTljLjM5LS4xOC42MS0uMjcuNjEtLjI3bC42MS4yNyAyLjIxLjk5Yy4zOS4xOC43MS40OS45MS44OGwuODkgMS44OWMuMTkuMzkuNDkuNzEuODguOTFsMS44OS44OWMuMzkuMTkuNzEuNDkuOTEuODhsLjk5IDIuMjFjLjE4LjM5LjI3LjYxLjI3LjYxbC0uMjcuNjEtLjk5IDIuMjFjLS4xOC4zOS0uNDkuNzEtLjg4LjkxbC0uODkgMS44OWMtLjE5LjM5LS40OS43MS0uODguOTFsLTIuMjEuOTljLS4zOS4xOC0uNjEuMjctLjYxLjI3eiIvPjxwYXRoIGQ9Ik0xMiAxN2MtLjU1IDAtMS0uNDUtMS0xczEuNDUtMSAxLTFjLjU1IDAgMSAuNDUgMSAxcy0uNDUgMS0xIDF6Ii8+PHBhdGggZD0iTTEyIDVjLS41NSAwLTEgLjQ1LTEgMXMxLjQ1IDEgMSAxcy0uNDUgMS0xIDFjLS41NSAwLTEtLjQ1LTEtMXMuNDUtMSAxLTF6Ii8+PC9zdmc+',
  location: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyQzguMTMgMiA1IDUuMTMgNSA5YzAgNS4yNSA3IDEzIDcgMTMgMCAwIDcgLTcuNzUgNy0xMyAwLTMuODctMy4xMy03LTctN3ptMCA5LjVjLTEuMzggMC0yLjUtMS4xMi0yLjUtMi41cy4xMi0yLjUgMi41LTIuNSAyLjUgMS4xMiAyLjUgMi41UzEzLjM4IDExLjUgMTIgMTEuNXoiLz48L3N2Zz4=',
  camera: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xOSA3aC00LjE4Yy0uNDEtMS4yMy0xLjUyLTIuMTMtMi44Mi0yLjEzUzkuNiA1Ljc3IDkuMTggN0g1Yy0xLjEgMC0yIC45LTIgMnYxMGMwIDEuMS45IDIgMiAyaDE0YzEuMSAwIDItLjkgMi0yVjljMC0xLjEtLjktMi0yLTJ6bS03IDdjLTEuNjYgMC0zLTEuMzQtMy0zczEuMzQtMyAzLTMgMyAxLjM0IDMgMy0xLjM0IDMtMyAzeiIvPjxwYXRoIGQ9Ik05IDEzaC0ydi0yaDJ2MnoiLz48L3N2Zz4=',
  calendar: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xOSA0aC0xVjJoLTJ2Mkg4VjJINlY0SDVjLTEuMSAwLTIgLjktMiAydjEzYzAgMS4xLjkgMiAyIDJoMTRjMS4xIDAgMi0uOSAyLTJWNmMwLTEuMS0uOS0yLTItMnptMCAxNUg1VjhoMTR2MTF6bS05LTVoMlY3aC0yeiIvPjwvc3ZnPg==',
  star: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAxNy4yN0w1LjIgMjFsMS40LTEuMjNMOSAxNi43NmwzLjQtNC45Nkw5IDE2Ljc2bDMuNCA0Ljk0IDEuNC0xLjIzTDEyIDE3LjI3eiIvPjxwYXRoIGQ9Ik0xMiA1LjE3TDkuNCA4LjQ4bDMuNi41Mi0yLjggMi44NS42NiAzLjk1TDEyIDEzLjIzbDEuMDQtMy45NS42Ni0zLjk1LTIuOC0yLjg1IDMuNi0uNTJMMTIgNS4xN3oiLz48L3N2Zz4=',
  close: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xOSA2LjQxTDE3LjU5IDUgMTIgMTAuNTkgNi40MSA1IDUgNi40MSAxMC41OSAxMiA1IDE3LjU5IDYuNDEgMTkgMTIgMTMuNDEgMTcuNTkgMTkgMTkgMTcuNTkgMTMuNDEgMTJ6Ii8+PC9zdmc+'
};

// 默认收藏旅行数据
const DEFAULT_FAVORITES = [
  { 
    id: '1', 
    title: '桂林山水之旅', 
    location: '广西桂林', 
    date: '2023-05-15', 
    image: 'https://picsum.photos/600/400?random=1',
    type: 'mountain',
    rating: 5,
    description: '漓江风光美不胜收,阳朔西街夜景迷人'
  },
  { 
    id: '2', 
    title: '三亚海滩度假', 
    location: '海南三亚', 
    date: '2023-04-22', 
    image: 'https://picsum.photos/600/400?random=2',
    type: 'beach',
    rating: 4,
    description: '亚龙湾沙滩细腻,海水清澈见底'
  },
  { 
    id: '3', 
    title: '北京文化探索', 
    location: '北京', 
    date: '2023-03-10', 
    image: 'https://picsum.photos/600/400?random=3',
    type: 'city',
    rating: 5,
    description: '故宫博物院历史悠久,长城雄伟壮观'
  },
  { 
    id: '4', 
    title: '张家界森林公园', 
    location: '湖南张家界', 
    date: '2023-02-18', 
    image: 'https://picsum.photos/600/400?random=4',
    type: 'forest',
    rating: 4,
    description: '天门山玻璃栈道惊险刺激,袁家界风景如画'
  }
];

const TravelFavorites: React.FC = () => {
  const [favorites, setFavorites] = useState(DEFAULT_FAVORITES);
  const [selectedTravel, setSelectedTravel] = useState<any>(null);
  const [modalVisible, setModalVisible] = useState(false);

  // 获取旅行类型图标
  const getTypeIcon = (type: string) => {
    switch (type) {
      case 'mountain': return ICONS.mountain;
      case 'beach': return ICONS.beach;
      case 'city': return ICONS.city;
      case 'forest': return ICONS.forest;
      default: return ICONS.mountain;
    }
  };

  // 获取旅行类型颜色
  const getTypeColor = (type: string) => {
    switch (type) {
      case 'mountain': return '#4361ee';
      case 'beach': return '#4cc9f0';
      case 'city': return '#f72585';
      case 'forest': return '#2ec4b6';
      default: return '#4361ee';
    }
  };

  // 移除收藏
  const removeFavorite = (id: string) => {
    Alert.alert(
      '取消收藏',
      '确定要从收藏中移除此旅行吗?',
      [
        { text: '取消', style: 'cancel' },
        { 
          text: '移除', 
          style: 'destructive', 
          onPress: () => {
            setFavorites(favorites.filter(travel => travel.id !== id));
            Alert.alert('已移除', '旅行已从收藏中移除');
          } 
        }
      ]
    );
  };

  // 查看旅行详情
  const viewTravelDetails = (travel: any) => {
    setSelectedTravel(travel);
    setModalVisible(true);
  };

  // 渲染星级评分
  const renderRating = (rating: number) => {
    return (
      <View style={styles.ratingContainer}>
        {[...Array(5)].map((_, i) => (
          <Text 
            key={i} 
            style={[styles.star, i < rating ? styles.filledStar : styles.emptyStar]}
          >
            {decodeURIComponent(escape(atob(ICONS.star.split(',')[1])))}
          </Text>
        ))}
      </View>
    );
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>❤️ 我的收藏</Text>
        <Text style={styles.subtitle}>珍藏的美好旅行回忆</Text>
        <View style={styles.statsContainer}>
          <View style={styles.statBox}>
            <Text style={styles.statNumber}>{favorites.length}</Text>
            <Text style={styles.statLabel}>收藏旅行</Text>
          </View>
        </View>
      </View>

      <ScrollView contentContainerStyle={styles.content}>
        {favorites.length === 0 ? (
          <View style={styles.emptyContainer}>
            <Text style={styles.emptyIcon}>{decodeURIComponent(escape(atob(ICONS.heart.split(',')[1])))}</Text>
            <Text style={styles.emptyText}>暂无收藏</Text>
            <Text style={styles.emptySubtext}>快去发现精彩旅行并收藏吧</Text>
          </View>
        ) : (
          favorites.map((travel) => (
            <View key={travel.id} style={styles.travelCard}>
              <View style={[styles.typeBadge, { backgroundColor: getTypeColor(travel.type) }]}>
                <Text style={styles.typeIcon}>
                  {decodeURIComponent(escape(atob(getTypeIcon(travel.type).split(',')[1])))}
                </Text>
              </View>
              
              <View style={styles.travelHeader}>
                <Text style={styles.travelTitle}>{travel.title}</Text>
                <TouchableOpacity 
                  style={styles.removeButton}
                  onPress={() => removeFavorite(travel.id)}
                >
                  <Text style={styles.removeIcon}>
                    {decodeURIComponent(escape(atob(ICONS.close.split(',')[1])))}
                  </Text>
                </TouchableOpacity>
              </View>
              
              <View style={styles.travelInfo}>
                <View style={styles.locationRow}>
                  <Text style={styles.locationIcon}>
                    {decodeURIComponent(escape(atob(ICONS.location.split(',')[1])))}
                  </Text>
                  <Text style={styles.travelLocation}>{travel.location}</Text>
                </View>
                
                <View style={styles.dateRow}>
                  <Text style={styles.dateIcon}>
                    {decodeURIComponent(escape(atob(ICONS.calendar.split(',')[1])))}
                  </Text>
                  <Text style={styles.travelDate}>{travel.date}</Text>
                </View>
                
                {renderRating(travel.rating)}
              </View>
              
              <Text style={styles.travelDescription}>{travel.description}</Text>
              
              <TouchableOpacity 
                style={styles.viewButton}
                onPress={() => viewTravelDetails(travel)}
              >
                <Text style={styles.viewButtonText}>查看详情</Text>
              </TouchableOpacity>
            </View>
          ))
        )}
      </ScrollView>

      {/* 旅行详情模态框 */}
      <Modal
        animationType="slide"
        transparent={true}
        visible={modalVisible}
        onRequestClose={() => setModalVisible(false)}
      >
        <View style={styles.modalOverlay}>
          <View style={styles.modalContent}>
            <View style={styles.modalHeader}>
              <Text style={styles.modalTitle}>旅行详情</Text>
              <TouchableOpacity onPress={() => setModalVisible(false)}>
                <Text style={styles.closeButton}>×</Text>
              </TouchableOpacity>
            </View>
            
            {selectedTravel && (
              <View style={styles.modalBody}>
                <View style={[styles.modalTypeBadge, { backgroundColor: getTypeColor(selectedTravel.type) }]}>
                  <Text style={styles.modalTypeIcon}>
                    {decodeURIComponent(escape(atob(getTypeIcon(selectedTravel.type).split(',')[1])))}
                  </Text>
                </View>
                
                <Text style={styles.modalTitleText}>{selectedTravel.title}</Text>
                
                <View style={styles.modalInfoRow}>
                  <Text style={styles.modalLabel}>地点:</Text>
                  <Text style={styles.modalValue}>{selectedTravel.location}</Text>
                </View>
                
                <View style={styles.modalInfoRow}>
                  <Text style={styles.modalLabel}>日期:</Text>
                  <Text style={styles.modalValue}>{selectedTravel.date}</Text>
                </View>
                
                <View style={styles.modalInfoRow}>
                  <Text style={styles.modalLabel}>评分:</Text>
                  <View style={styles.modalRating}>
                    {renderRating(selectedTravel.rating)}
                  </View>
                </View>
                
                <View style={styles.modalInfoRow}>
                  <Text style={styles.modalLabel}>描述:</Text>
                  <Text style={styles.modalDescription}>{selectedTravel.description}</Text>
                </View>
              </View>
            )}
            
            <View style={styles.modalActions}>
              <TouchableOpacity 
                style={[styles.modalButton, styles.modalRemoveButton]}
                onPress={() => {
                  removeFavorite(selectedTravel?.id);
                  setModalVisible(false);
                }}
              >
                <Text style={styles.modalButtonText}>取消收藏</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </Modal>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  header: {
    paddingTop: 30,
    paddingBottom: 20,
    paddingHorizontal: 20,
    backgroundColor: '#ffffff',
    borderBottomWidth: 1,
    borderBottomColor: '#e9ecef',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#212529',
    textAlign: 'center',
  },
  subtitle: {
    fontSize: 14,
    color: '#6c757d',
    textAlign: 'center',
    marginTop: 4,
  },
  statsContainer: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginTop: 15,
  },
  statBox: {
    alignItems: 'center',
    paddingHorizontal: 20,
    paddingVertical: 10,
    backgroundColor: '#e9ecef',
    borderRadius: 12,
  },
  statNumber: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#212529',
  },
  statLabel: {
    fontSize: 14,
    color: '#6c757d',
    marginTop: 2,
  },
  content: {
    padding: 16,
  },
  emptyContainer: {
    alignItems: 'center',
    justifyContent: 'center',
    paddingVertical: 60,
  },
  emptyIcon: {
    fontSize: 64,
    color: '#ced4da',
    marginBottom: 20,
  },
  emptyText: {
    fontSize: 20,
    fontWeight: '600',
    color: '#495057',
    marginBottom: 8,
  },
  emptySubtext: {
    fontSize: 16,
    color: '#6c757d',
  },
  travelCard: {
    backgroundColor: '#ffffff',
    borderRadius: 16,
    padding: 20,
    marginBottom: 16,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    position: 'relative',
  },
  typeBadge: {
    position: 'absolute',
    top: -12,
    right: 20,
    width: 40,
    height: 40,
    borderRadius: 20,
    alignItems: 'center',
    justifyContent: 'center',
    zIndex: 1,
  },
  typeIcon: {
    fontSize: 20,
    color: '#ffffff',
  },
  travelHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 15,
  },
  travelTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#212529',
    flex: 1,
  },
  removeButton: {
    padding: 8,
    marginLeft: 10,
  },
  removeIcon: {
    fontSize: 20,
    color: '#adb5bd',
  },
  travelInfo: {
    marginBottom: 15,
  },
  locationRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 8,
  },
  locationIcon: {
    fontSize: 16,
    color: '#4361ee',
    marginRight: 8,
  },
  travelLocation: {
    fontSize: 16,
    color: '#495057',
    fontWeight: '600',
  },
  dateRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 10,
  },
  dateIcon: {
    fontSize: 16,
    color: '#4cc9f0',
    marginRight: 8,
  },
  travelDate: {
    fontSize: 14,
    color: '#6c757d',
  },
  ratingContainer: {
    flexDirection: 'row',
    marginTop: 5,
  },
  star: {
    fontSize: 16,
    marginRight: 2,
  },
  filledStar: {
    color: '#ffd43b',
  },
  emptyStar: {
    color: '#e9ecef',
  },
  travelDescription: {
    fontSize: 14,
    color: '#6c757d',
    lineHeight: 20,
    marginBottom: 20,
  },
  viewButton: {
    backgroundColor: '#4361ee',
    paddingVertical: 12,
    borderRadius: 10,
    alignItems: 'center',
  },
  viewButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#ffffff',
  },
  modalOverlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  modalContent: {
    backgroundColor: '#ffffff',
    width: '85%',
    borderRadius: 20,
    padding: 25,
    maxHeight: '80%',
  },
  modalHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 20,
  },
  modalTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#212529',
  },
  closeButton: {
    fontSize: 30,
    color: '#adb5bd',
    fontWeight: '200',
  },
  modalBody: {
    marginBottom: 25,
  },
  modalTypeBadge: {
    width: 50,
    height: 50,
    borderRadius: 25,
    alignItems: 'center',
    justifyContent: 'center',
    alignSelf: 'center',
    marginBottom: 15,
  },
  modalTypeIcon: {
    fontSize: 24,
    color: '#ffffff',
  },
  modalTitleText: {
    fontSize: 22,
    fontWeight: 'bold',
    color: '#212529',
    textAlign: 'center',
    marginBottom: 20,
  },
  modalInfoRow: {
    flexDirection: 'row',
    marginBottom: 15,
    alignItems: 'flex-start',
  },
  modalLabel: {
    fontSize: 16,
    fontWeight: '600',
    color: '#495057',
    width: 60,
  },
  modalValue: {
    flex: 1,
    fontSize: 16,
    color: '#212529',
  },
  modalRating: {
    flex: 1,
  },
  modalDescription: {
    flex: 1,
    fontSize: 16,
    color: '#212529',
    lineHeight: 22,
  },
  modalActions: {
    flexDirection: 'row',
    justifyContent: 'center',
  },
  modalButton: {
    flex: 1,
    paddingVertical: 15,
    borderRadius: 12,
    alignItems: 'center',
    marginHorizontal: 5,
  },
  modalRemoveButton: {
    backgroundColor: '#e63946',
  },
  modalButtonText: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#ffffff',
  },
});

export default TravelFavorites;

这段代码实现了一个旅行收藏管理界面,主要用于展示和管理用户收藏的旅行信息。从鸿蒙开发的角度来看,这个组件展现了现代移动应用开发的核心设计理念和实现方式。

在数据结构设计上,DEFAULT_FAVORITES数组采用了ID、标题、位置、日期、图片、类型、评分和描述的组合方式来定义旅行收藏对象。这种设计在鸿蒙应用开发中同样适用,鸿蒙的ArkTS语言支持类似的对象数组结构,可以使用interface来定义旅行收藏对象的类型结构,确保数据的一致性和类型安全。鸿蒙开发中推荐使用资源管理机制来处理图标,将图标文件放置在resources目录下,通过$r(‘app.media.icon_name’)方式引用。

在状态管理方面,React使用useState来维护组件状态,包括收藏列表、选中旅行、模态框显示状态等。鸿蒙开发中可以使用@State装饰器实现类似的状态管理机制,通过状态变量的变更来驱动UI的自动更新。鸿蒙的声明式UI框架同样具有高效的渲染机制,通过状态变化自动计算最小渲染代价来更新界面。

UI布局采用了卡片列表设计,通过ScrollView容器展示旅行收藏卡片。在鸿蒙开发中,可以使用Column和Row组合配合ForEach循环渲染来实现类似的列表布局效果。每个旅行卡片包含了图片展示区、标题信息区、位置详情区和操作按钮区,这种模块化的设计便于维护和扩展。

请添加图片描述

旅行类型图标处理根据旅行类型动态选择不同的图标和颜色,这种设计在鸿蒙应用中同样重要。鸿蒙支持通过条件渲染来实现类似的功能,根据旅行类型动态绑定不同的图标资源和样式属性。颜色管理方面,为不同旅行类型指定了特定的主题色,这在鸿蒙应用中可以通过资源文件统一管理颜色值。

模态框的实现体现了良好的用户体验设计,通过透明遮罩来突出操作焦点。鸿蒙系统提供了丰富的弹窗组件,可以实现更加原生和一致的用户交互体验。旅行详情查看功能可以通过鸿蒙的Sheet组件或者自定义弹窗来实现。

数据持久化方面,虽然代码中没有直接体现,但在实际应用中会将收藏数据存储在本地或云端。鸿蒙提供了多种数据存储方案,包括Preferences轻量级数据存储、KVStore分布式数据存储等,可以根据应用需求选择合适的存储方式。

在交互设计上,移除收藏操作提供了确认机制,防止误操作。鸿蒙系统有内置的AlertDialog组件,可以提供更加原生和一致的用户交互体验。星级评分展示通过循环渲染多个星星图标来实现,这种数据可视化在鸿蒙应用中可以通过ForEach循环结合条件渲染来实现。


打包

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

在这里插入图片描述

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

在这里插入图片描述

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

请添加图片描述

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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐