在电商应用中,搜索功能是用户快速找到目标商品的关键入口,其设计质量直接影响用户体验和购物效率。本文将深入分析一个基于 React Native 实现的商品搜索系统,探讨其架构设计、技术实现以及鸿蒙跨端适配策略。

状态管理

该搜索系统采用了 React Hooks 中的 useState 进行轻量级状态管理,构建了多层次的状态模型:

const [searchQuery, setSearchQuery] = useState<string>('');
const [searchResults, setSearchResults] = useState<Product[]>([]);
const [searchHistories, setSearchHistories] = useState<SearchHistory[]>([
  // 历史记录...
]);
const [showFilters, setShowFilters] = useState<boolean>(false);
const [sortBy, setSortBy] = useState<string>('relevance');
const [minPrice, setMinPrice] = useState<string>('');
const [maxPrice, setMaxPrice] = useState<string>('');
const [selectedBrand, setSelectedBrand] = useState<string>('all');

这种状态管理方式具有以下优势:

  • 类型安全:通过 TypeScript 接口 ProductSearchHistory 确保状态结构一致性
  • 模块化:将搜索查询、结果、历史记录和筛选条件分离管理
  • 响应式:状态变更自动触发组件重渲染
  • 跨端兼容:React Hooks 在鸿蒙系统的 React Native 实现中通常都有良好支持

搜索机制

系统实现了完整的搜索流程:

const performSearch = () => {
  if (searchQuery.trim() === '') {
    setSearchResults([]);
    return;
  }

  // 添加到搜索历史
  const newHistory: SearchHistory = {
    id: `h${Date.now()}`,
    query: searchQuery,
    timestamp: new Date()
  };
  setSearchHistories(prev => [newHistory, ...prev.slice(0, 9)]);

  // 模拟搜索结果
  const results = products.filter(product =>
    product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
    product.category.toLowerCase().includes(searchQuery.toLowerCase())
  );
  setSearchResults(results);
};

搜索流程包括:

  • 输入验证:空搜索词时清空结果
  • 历史记录管理:添加新搜索到历史记录,限制历史记录数量
  • 结果筛选:根据搜索词匹配商品名称和分类
  • 结果更新:更新搜索结果状态

筛选与排序

系统实现了多维度的商品筛选和排序功能:

const applyFilters = () => {
  let filtered = [...searchResults];
  
  // 按价格过滤
  if (minPrice) {
    filtered = filtered.filter(p => p.price >= parseInt(minPrice));
  }
  if (maxPrice) {
    filtered = filtered.filter(p => p.price <= parseInt(maxPrice));
  }
  
  // 按品牌过滤
  if (selectedBrand !== 'all') {
    filtered = filtered.filter(p => p.store.includes(selectedBrand));
  }
  
  // 排序逻辑...
};

筛选功能支持:

  • 价格区间:设置最低和最高价格
  • 品牌筛选:按品牌过滤商品
  • 多维度排序:支持按相关性、价格、评分等排序

基础架构

该实现采用了 React Native 核心组件库,确保了在鸿蒙系统上的基本兼容性:

  • SafeAreaView:适配刘海屏等异形屏
  • ScrollView:处理内容滚动
  • TouchableOpacity:提供触摸反馈
  • FlatList:高效渲染搜索结果
  • TextInput:提供搜索输入功能
  • Alert:系统级弹窗提示

Base64 图标

系统使用 Base64 编码的图标库:

const ICONS_BASE64 = {
  search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  // 其他图标...
};

这种处理方式在跨端开发中尤为重要:

  • 避免了不同平台对资源文件格式的兼容性问题
  • 减少了网络请求,提高了加载速度
  • 简化了构建流程,无需处理多平台资源文件

屏幕尺寸

系统通过 Dimensions API 获取屏幕尺寸:

const { width, height } = Dimensions.get('window');

这种方式确保了在不同屏幕尺寸的设备上都能获得一致的布局体验,无论是 React Native 环境还是鸿蒙系统。


系统采用了模块化的样式定义:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  // 其他样式...
});

这种方式确保了样式的一致性和可维护性,同时为后续的主题定制和深色模式适配预留了扩展空间。


在鸿蒙系统上使用 React Native 时,应注意以下 API 兼容性问题:

  1. FlatList API:鸿蒙系统的 FlatList 实现可能与 React Native 有所差异,建议测试确认滚动和渲染行为
  2. TextInput API:鸿蒙系统的 TextInput 实现可能与 React Native 有所差异,建议测试确认输入行为
  3. Alert API:鸿蒙系统的 Alert 实现可能与 React Native 有所差异,建议测试确认弹窗行为

  1. 类型定义

    // 更详细的商品类型
    interface Product {
      id: string;
      name: string;
      price: number;
      originalPrice?: number;
      rating: number;
      reviewCount: number;
      store: string;
      category: string;
      imageUrl?: string;
      isFavorite: boolean;
      discount?: number;
      tags?: string[];
      stock?: number;
      sales?: number;
    }
    
    // 更详细的搜索历史类型
    interface SearchHistory {
      id: string;
      query: string;
      timestamp: Date;
      count?: number; // 搜索次数
    }
    
  2. 状态管理

    // 使用 useReducer 管理复杂状态
    const [state, dispatch] = useReducer(searchReducer, {
      products: initialProducts,
      searchQuery: '',
      searchResults: [],
      searchHistories: initialHistories,
      showFilters: false,
      sortBy: 'relevance',
      minPrice: '',
      maxPrice: '',
      selectedBrand: 'all',
      loading: false,
      error: null
    });
    
  3. 性能

    // 使用 useMemo 缓存搜索结果
    const searchResults = useMemo(() => {
      if (!searchQuery.trim()) return [];
      return products.filter(product =>
        product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
        product.category.toLowerCase().includes(searchQuery.toLowerCase())
      );
    }, [products, searchQuery]);
    
    // 使用防抖优化搜索
    const debouncedSearch = useCallback(
      debounce((query) => {
        // 搜索逻辑
      }, 300),
      []
    );
    
  4. 错误处理

    // 添加错误边界
    const performSearch = () => {
      try {
        // 搜索逻辑
      } catch (error) {
        console.error('搜索失败:', error);
        Alert.alert('错误', '搜索失败,请稍后重试');
      }
    };
    
  5. 可访问性

    // 添加可访问性标签
    <TextInput
      accessible={true}
      accessibilityLabel="搜索输入"
      // 其他属性
    />
    

本商品搜索系统实现了一个功能完整、用户友好的搜索界面,通过合理的架构设计和代码组织,为用户提供了良好的购物体验。在跨端开发场景下,该实现充分考虑了 React Native 和鸿蒙系统的兼容性需求,为后续的功能扩展和性能优化预留了空间。


商品搜索功能作为电商应用的核心流量入口,其交互体验、性能表现和功能完整性直接决定用户的商品查找效率与转化路径。本文将深度拆解这份基于 React Native 构建的搜索页代码,从数据模型设计、搜索交互逻辑、状态管理体系、UI 布局架构四个维度剖析其技术内核,并结合鸿蒙(HarmonyOS)跨端开发实践,提供一套完整的 React Native 到鸿蒙 ArkTS 的适配方案,为跨端电商应用的搜索模块开发提供可落地的技术参考。

1. 数据模型

搜索页的核心价值在于对商品数据的多维度检索与展示,代码首先通过 TypeScript 构建了严谨的类型体系,为后续的状态管理和数据处理奠定基础:

(1)核心数据
// 商品核心类型:覆盖电商商品全维度属性
type Product = {
  id: string;
  name: string;
  price: number;
  originalPrice?: number;
  rating: number;
  reviewCount: number;
  store: string;
  category: string;
  imageUrl?: string;
  isFavorite: boolean;
  discount?: number;
  tags?: string[];
};

// 搜索历史类型:记录用户检索行为
type SearchHistory = {
  id: string;
  query: string;
  timestamp: Date;
};

设计亮点分析:

  • Product 类型覆盖了商品展示所需的全维度属性,包括价格体系(原价/现价/折扣)、评价体系(评分/评论数)、业务标签(热销/正品/新品)等核心电商属性,可选属性的合理使用兼顾了数据灵活性与类型安全性;
  • SearchHistory 类型新增 timestamp 字段,为后续搜索历史的时间排序、过期清理等扩展功能提供数据支撑,符合电商产品的用户行为分析需求。
(2)多维度状态

搜索页的交互核心是状态的动态变更,代码通过 useState 构建了分层的状态管理体系,精准把控搜索全流程的状态流转:

// 核心数据状态
const [products] = useState<Product[]>([/* 商品数据源 */]);

// 搜索核心状态
const [searchQuery, setSearchQuery] = useState<string>('');
const [searchResults, setSearchResults] = useState<Product[]>([]);
const [searchHistories, setSearchHistories] = useState<SearchHistory[]>([/* 历史数据 */]);

// 筛选排序状态
const [showFilters, setShowFilters] = useState<boolean>(false);
const [sortBy, setSortBy] = useState<string>('relevance');
const [minPrice, setMinPrice] = useState<string>('');
const [maxPrice, setMaxPrice] = useState<string>('');
const [selectedBrand, setSelectedBrand] = useState<string>('all');

状态设计遵循单一职责原则

  • 核心数据状态(products):存储基础商品数据源,初始化后不再变更(实际项目中可对接 API 动态更新);
  • 搜索核心状态:
    • searchQuery:存储搜索关键词,是整个搜索流程的核心驱动状态;
    • searchResults:存储搜索结果集,与搜索关键词实时联动;
    • searchHistories:存储用户搜索历史,支持增删清全量操作;
  • 筛选排序状态:
    • showFilters:控制筛选面板的显隐状态;
    • sortBy:管理排序规则(相关性/价格升序/价格降序/评分);
    • minPrice/maxPrice:存储价格区间筛选条件;
    • selectedBrand:存储品牌筛选条件,实现商品的精准过滤。

2. 搜索核心逻辑

搜索页的核心价值在于帮助用户快速定位目标商品,代码实现了关键词检索 + 历史管理 + 多维度筛选排序的完整搜索逻辑体系:

(1)关键词检索
const performSearch = () => {
  if (searchQuery.trim() === '') {
    setSearchResults([]);
    return;
  }

  // 添加到搜索历史(限制最多10条)
  const newHistory: SearchHistory = {
    id: `h${Date.now()}`,
    query: searchQuery,
    timestamp: new Date()
  };
  setSearchHistories(prev => [newHistory, ...prev.slice(0, 9)]);

  // 多维度模糊匹配:商品名称 + 分类
  const results = products.filter(product => 
    product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
    product.category.toLowerCase().includes(searchQuery.toLowerCase())
  );
  setSearchResults(results);
};

技术亮点分析:

  • 输入校验:通过 trim() 过滤空关键词,避免无效搜索请求;
  • 历史管理优化:新增历史记录时通过 slice(0, 9) 限制最多存储10条历史,防止数据冗余,符合移动端交互习惯;
  • 大小写不敏感匹配:通过 toLowerCase() 统一字符大小写,避免因大小写差异导致的搜索遗漏;
  • 多维度匹配:同时匹配商品名称和分类,提升搜索结果的覆盖率,优化用户搜索体验。
(2)搜索历史管理
// 清空全部历史
const clearSearchHistory = () => {
  setSearchHistories([]);
  Alert.alert('清除成功', '搜索历史已清除');
};

// 删除单条历史
const removeSearchHistory = (id: string) => {
  setSearchHistories(prev => prev.filter(history => history.id !== id));
};

设计亮点:

  • 提供清空全部和删除单条两种操作,满足用户不同的历史管理需求;
  • 清空操作后给出弹窗反馈,符合移动端交互的反馈原则,提升用户操作感知。
(3)多维度筛选排序
const applyFilters = () => {
  let filtered = [...searchResults];
  
  // 价格区间过滤
  if (minPrice) filtered = filtered.filter(p => p.price >= parseInt(minPrice));
  if (maxPrice) filtered = filtered.filter(p => p.price <= parseInt(maxPrice));
  
  // 品牌过滤(通过店铺名称包含品牌关键词实现)
  if (selectedBrand !== 'all') filtered = filtered.filter(p => p.store.includes(selectedBrand));
  
  // 多维度排序
  filtered.sort((a, b) => {
    if (sortBy === 'price-low') return a.price - b.price;
    if (sortBy === 'price-high') return b.price - a.price;
    if (sortBy === 'rating') return b.rating - a.rating;
    return 0; // 默认相关性排序
  });
  
  setSearchResults(filtered);
  setShowFilters(false);
};

技术亮点:

  • 不可变数据处理:通过扩展运算符 [...searchResults] 创建新数组,避免修改原数组引用,符合 React 不可变数据的设计理念;
  • 渐进式过滤:依次执行价格过滤、品牌过滤、排序操作,逻辑清晰且易于扩展;
  • 品牌过滤巧思:通过 store.includes(selectedBrand) 实现品牌筛选,无需额外维护品牌映射表,简化数据结构;
  • 排序算法优化:通过简单的比较函数实现多维度排序,算法时间复杂度为 O(n log n),在移动端商品列表量级下性能表现优异。

3. 交互体验

搜索页的布局架构直接决定用户体验,代码采用搜索栏 + 动态内容区(建议/结果) + 筛选面板 + 底部导航的经典电商搜索布局,结合精细化的视觉交互设计,打造了符合用户直觉的操作体验。

(1)整体布局
SafeAreaView
├── Header(头部:搜索栏 + 搜索按钮)
├── ScrollView(滚动内容区)
│   ├── SuggestionSection(搜索建议:热门搜索 + 历史记录)
│   ├── ResultSection(搜索结果:结果统计 + 筛选排序 + 商品列表)
│   └── EmptyResult(空结果提示)
├── FilterPanel(筛选面板,条件渲染)
└── BottomNav(底部导航)

布局设计优势:

  • 动态内容区设计:根据 searchQuery 状态动态切换搜索建议和搜索结果,无关键词时展示热门搜索和历史记录,有关键词时展示搜索结果,符合用户搜索的认知流程;
  • 空状态处理:专门设计空搜索结果提示区域,引导用户尝试其他关键词或查看热门搜索,提升用户留存率;
  • 筛选面板层级优化:采用绝对定位 + 半透明遮罩层实现筛选面板,层级清晰且不破坏主布局结构;
  • 滚动容器适配:使用 ScrollView 包裹动态内容区,保证在内容超出屏幕时的可滚动性,适配不同设备屏幕尺寸。

搜索栏组件
<View style={styles.header}>
  <View style={styles.searchContainer}>
    <Text style={styles.searchIcon}>🔍</Text>
    <TextInput
      style={styles.searchInput}
      placeholder="搜索商品..."
      value={searchQuery}
      onChangeText={setSearchQuery}
      onSubmitEditing={performSearch}
      returnKeyType="search"
    />
    {searchQuery.length > 0 && (
      <TouchableOpacity 
        style={styles.clearButton}
        onPress={() => setSearchQuery('')}
      >
        <Text style={styles.clearButtonText}>×</Text>
      </TouchableOpacity>
    )}
  </View>
  <TouchableOpacity 
    style={styles.searchButton}
    onPress={performSearch}
  >
    <Text style={styles.searchButtonText}>搜索</Text>
  </TouchableOpacity>
</View>

交互设计亮点:

  • 输入反馈优化:输入框绑定 onSubmitEditing 事件,支持键盘搜索按钮触发搜索,符合移动端输入习惯;
  • 清空按钮条件渲染:仅在有输入内容时显示清空按钮,避免界面冗余;
  • 搜索按钮独立设计:提供独立的搜索按钮,兼容不同用户的操作习惯(键盘/点击)。
筛选面板组件
{showFilters && (
  <View style={styles.filterPanel}>
    <View style={styles.filterPanelHeader}>
      <Text style={styles.filterPanelTitle}>筛选条件</Text>
      <TouchableOpacity 
        style={styles.closeFilterButton}
        onPress={() => setShowFilters(false)}
      >
        <Text style={styles.closeFilterText}>×</Text>
      </TouchableOpacity>
    </View>
    
    {/* 价格区间筛选 */}
    <View style={styles.filterOption}>
      <Text style={styles.filterOptionText}>价格区间</Text>
      <View style={styles.priceRange}>
        <TextInput
          style={styles.priceInput}
          placeholder="最低价"
          keyboardType="numeric"
          value={minPrice}
          onChangeText={setMinPrice}
        />
        <Text style={styles.toText}>-</Text>
        <TextInput
          style={styles.priceInput}
          placeholder="最高价"
          keyboardType="numeric"
          value={maxPrice}
          onChangeText={setMaxPrice}
        />
      </View>
    </View>
    
    {/* 品牌筛选 */}
    <View style={styles.filterOption}>
      <Text style={styles.filterOptionText}>品牌</Text>
      <View style={styles.brandOptions}>
        {['all', '苹果', '小米', ...].map(brand => (
          <TouchableOpacity 
            key={brand}
            style={[
              styles.brandButton, 
              selectedBrand === brand && styles.selectedBrandButton
            ]}
            onPress={() => setSelectedBrand(brand)}
          >
            <Text style={[
              styles.brandButtonText,
              selectedBrand === brand && styles.selectedBrandButtonText
            ]}>
              {brand === 'all' ? '全部' : brand}
            </Text>
          </TouchableOpacity>
        ))}
      </View>
    </View>
    
    {/* 操作按钮 */}
    <View style={styles.filterActions}>
      <TouchableOpacity style={styles.resetButton} onPress={() => {/* 重置逻辑 */}}>
        <Text style={styles.resetButtonText}>重置</Text>
      </TouchableOpacity>
      <TouchableOpacity style={styles.applyButton} onPress={applyFilters}>
        <Text style={styles.applyButtonText}>应用</Text>
      </TouchableOpacity>
    </View>
  </View>
)}

设计亮点:

  • 面板层级设计:采用底部弹出式面板 + 半透明遮罩,符合移动端筛选面板的交互范式;
  • 输入优化:价格输入框指定 keyboardType="numeric",提升移动端数字输入体验;
  • 选中态视觉强化:品牌按钮通过背景色和文字颜色的变化清晰展示选中状态,提升用户操作感知;
  • 操作按钮语义化:提供“重置”和“应用”按钮,分别对应筛选条件的清空和生效,逻辑清晰。
(3)样式

代码通过 StyleSheet.create 构建了完整的样式体系,遵循复用性 + 语义化 + 视觉一致性原则:

const styles = StyleSheet.create({
  // 容器样式
  container: { flex: 1, backgroundColor: '#f5f7fa' },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    backgroundColor: '#ffffff',
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  
  // 搜索栏样式
  searchContainer: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f1f5f9',
    borderRadius: 20,
    paddingVertical: 10,
    paddingHorizontal: 16,
    marginRight: 10,
  },
  
  // 商品卡片样式
  productCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 12,
    flex: 0.48,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    position: 'relative',
  },
  
  // 筛选面板样式
  filterPanel: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0,0,0,0.5)',
    zIndex: 1000,
    justifyContent: 'flex-end',
  },
  
  // 其他样式...
});

样式设计核心原则:

  • 色彩体系统一:主色调采用蓝色(#3b82f6),警示色采用红色(#ef4444),中性色采用灰度渐变,符合电商视觉设计规范;
  • 阴影与层级:通过 elevation(安卓)和 shadow(iOS)属性,为卡片组件添加轻微阴影,提升视觉层次感;
  • 弹性布局:商品卡片采用 flex: 0.48 配合 justifyContent: 'space-between',实现双列布局的均匀分布;
  • 圆角统一:核心组件统一使用 12px 圆角,符合现代移动端 UI 设计趋势;
  • 响应式适配:通过 flex 布局和百分比宽度,保证在不同设备上的布局比例一致。

将 React Native 搜索页迁移至鸿蒙平台,核心是基于 ArkTS + ArkUI 实现数据模型、状态管理、布局架构、交互逻辑的对等还原,以下是关键适配技术点:

1. 数据模型

RN 的 TypeScript 类型体系可无缝迁移至鸿蒙 ArkTS,仅需调整类型定义语法,核心字段完全复用:

(1)数据类型
// RN 类型定义
type Product = {
  id: string;
  name: string;
  price: number;
  originalPrice?: number;
  rating: number;
  reviewCount: number;
  store: string;
  category: string;
  imageUrl?: string;
  isFavorite: boolean;
  discount?: number;
  tags?: string[];
};

type SearchHistory = {
  id: string;
  query: string;
  timestamp: Date;
};

// 鸿蒙 ArkTS 适配
interface Product {
  id: string;
  name: string;
  price: number;
  originalPrice?: number;
  rating: number;
  reviewCount: number;
  store: string;
  category: string;
  imageUrl?: string;
  isFavorite: boolean;
  discount?: number;
  tags?: string[];
}

interface SearchHistory {
  id: string;
  query: string;
  timestamp: Date;
}
(2)状态管理

RN 的 useState 对应鸿蒙的 @State 装饰器,状态初始化与更新逻辑完全复用:

@Entry
@Component
struct SearchApp {
  // 核心数据状态
  @State products: Product[] = [
    { id: '1', name: 'iPhone 15 Pro Max', price: 9999, originalPrice: 10999, rating: 4.8, reviewCount: 1250, store: '苹果官方旗舰店', category: '手机', isFavorite: false, discount: 10, tags: ['热销', '正品'] },
    // 其他商品数据...
  ];
  
  // 搜索核心状态
  @State searchQuery: string = '';
  @State searchResults: Product[] = [];
  @State searchHistories: SearchHistory[] = [
    { id: 'h1', query: 'iPhone', timestamp: new Date(Date.now() - 86400000) },
    // 其他历史数据...
  ];
  
  // 筛选排序状态
  @State showFilters: boolean = false;
  @State sortBy: string = 'relevance';
  @State minPrice: string = '';
  @State maxPrice: string = '';
  @State selectedBrand: string = 'all';
  
  // 搜索逻辑(完全复用 RN 逻辑)
  performSearch() {
    if (this.searchQuery.trim() === '') {
      this.searchResults = [];
      return;
    }

    const newHistory: SearchHistory = {
      id: `h${Date.now()}`,
      query: this.searchQuery,
      timestamp: new Date()
    };
    this.searchHistories = [newHistory, ...this.searchHistories.slice(0, 9)];

    const results = this.products.filter(product => 
      product.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
      product.category.toLowerCase().includes(this.searchQuery.toLowerCase())
    );
    this.searchResults = results;
  }
  
  // 筛选逻辑(完全复用 RN 逻辑)
  applyFilters() {
    let filtered = [...this.searchResults];
    
    if (this.minPrice) filtered = filtered.filter(p => p.price >= parseInt(this.minPrice));
    if (this.maxPrice) filtered = filtered.filter(p => p.price <= parseInt(this.maxPrice));
    
    if (this.selectedBrand !== 'all') filtered = filtered.filter(p => p.store.includes(this.selectedBrand));
    
    filtered.sort((a, b) => {
      if (this.sortBy === 'price-low') return a.price - b.price;
      if (this.sortBy === 'price-high') return b.price - a.price;
      if (this.sortBy === 'rating') return b.rating - a.rating;
      return 0;
    });
    
    this.searchResults = filtered;
    this.showFilters = false;
  }
  
  // 其他交互函数...
}

适配亮点:

  • 计算逻辑完全复用:搜索、筛选、排序等核心业务逻辑无需修改,直接迁移即可运行;
  • 状态更新语法对齐:RN 的 setXXX 对应鸿蒙的直接赋值(this.xxx = value),学习成本低;
  • 数据不可变性保持:继续使用扩展运算符创建新数组,保持 React 生态的不可变数据设计理念。

ArkUI 的布局体系与 React Native 高度对齐,核心组件映射关系及适配方案如下:

React Native 组件 鸿蒙 ArkUI 对应实现 适配关键说明
SafeAreaView Column().safeArea(true) 安全区域适配,避免状态栏遮挡
View Column/Row/Stack 根据布局方向选择容器组件
Text Text 文本样式、换行、对齐方式完全复用
TextInput TextInput 输入框属性高度兼容,仅调整事件名
TouchableOpacity Button().stateEffect(true) 点击反馈通过 stateEffect 实现
FlatList List 列表渲染核心组件,支持网格/横向布局
ScrollView Scroll 滚动容器,属性和用法高度一致
StyleSheet 链式样式 + @Styles 样式定义语法调整,视觉效果一致
(1)搜索栏
// RN 搜索栏
<View style={styles.header}>
  <View style={styles.searchContainer}>
    <Text style={styles.searchIcon}>🔍</Text>
    <TextInput
      style={styles.searchInput}
      placeholder="搜索商品..."
      value={searchQuery}
      onChangeText={setSearchQuery}
      onSubmitEditing={performSearch}
      returnKeyType="search"
    />
    {searchQuery.length > 0 && (
      <TouchableOpacity onPress={() => setSearchQuery('')}>
        <Text>×</Text>
      </TouchableOpacity>
    )}
  </View>
  <TouchableOpacity onPress={performSearch}>
    <Text>搜索</Text>
  </TouchableOpacity>
</View>

// 鸿蒙搜索栏适配
Row()
  .alignItems(ItemAlign.Center)
  .padding(16)
  .backgroundColor('#ffffff')
  .borderBottom({ width: 1, color: '#e2e8f0' }) {
  
  Row()
    .flexGrow(1)
    .alignItems(ItemAlign.Center)
    .backgroundColor('#f1f5f9')
    .borderRadius(20)
    .padding({ top: 10, bottom: 10, left: 16, right: 16 })
    .marginRight(10) {
    
    Text('🔍')
      .fontSize(18)
      .fontColor('#64748b')
      .marginRight(8);
    
    TextInput()
      .flexGrow(1)
      .fontSize(14)
      .fontColor('#1e293b')
      .placeholder('搜索商品...')
      .value(this.searchQuery)
      .onChange((value) => {
        this.searchQuery = value;
      })
      .onSubmit(() => {
        this.performSearch();
      })
      .inputSubmitKey(InputSubmitKey.Search);
    
    if (this.searchQuery.length > 0) {
      Button()
        .width(20)
        .height(20)
        .borderRadius(10)
        .backgroundColor('#cbd5e1')
        .onClick(() => {
          this.searchQuery = '';
        }) {
          Text('×')
            .fontSize(14)
            .fontColor('#64748b')
            .fontWeight(FontWeight.Bold);
        }
    }
  }
  
  Button()
    .backgroundColor('#3b82f6')
    .padding({ left: 16, right: 16, top: 10, bottom: 10 })
    .borderRadius(6)
    .onClick(() => {
      this.performSearch();
    }) {
      Text('搜索')
        .fontSize(14)
        .fontColor('#ffffff')
        .fontWeight(FontWeight.Medium);
    }
}
(2)商品列表
// RN 商品列表
<FlatList
  data={searchResults}
  renderItem={renderProduct}
  keyExtractor={item => item.id}
  numColumns={2}
  columnWrapperStyle={styles.productRow}
  showsVerticalScrollIndicator={false}
  contentContainerStyle={styles.productList}
/>

// 鸿蒙商品列表适配
List()
  .gridCount(2) // 对应 RN 的 numColumns={2}
  .showsVerticalScrollbar(false)
  .paddingBottom(80) {
  
  ForEach(this.searchResults, (product: Product) => {
    ListItem() {
      Column()
        .backgroundColor('#ffffff')
        .borderRadius(12)
        .padding(12)
        .flexGrow(0.48)
        .shadow({ color: '#000', offsetX: 0, offsetY: 1, opacity: 0.1, radius: 2 })
        .onClick(() => {
          // 商品详情逻辑
        }) {
        
        // 商品图片占位
        Column()
          .width('100%')
          .height(120)
          .borderRadius(8)
          .backgroundColor('#f1f5f9')
          .justifyContent(FlexAlign.Center)
          .alignItems(ItemAlign.Center)
          .marginBottom(8) {
            Text('📱')
              .fontSize(30);
          }
        
        // 商品名称
        Text(product.name)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor('#1e293b')
          .marginBottom(8)
          .maxLines(2);
        
        // 价格区域
        Row()
          .alignItems(ItemAlign.Center)
          .marginBottom(4) {
          
          Text(`¥${product.price}`)
            .fontSize(16)
            .fontColor('#ef4444')
            .fontWeight(FontWeight.Bold)
            .marginRight(8);
          
          if (product.originalPrice) {
            Text(`¥${product.originalPrice}`)
              .fontSize(12)
              .fontColor('#94a3b8')
              .decoration({ type: TextDecorationType.LineThrough })
              .marginRight(8);
          }
          
          if (product.discount) {
            Text(`-${product.discount}%`)
              .fontSize(10)
              .fontColor('#ffffff')
              .backgroundColor('#ef4444')
              .padding({ left: 4, right: 4, top: 2, bottom: 2 })
              .borderRadius(4);
          }
        }
        
        // 其他商品信息...
        
        // 操作按钮
        Row()
          .justifyContent(FlexAlign.SpaceBetween)
          .marginTop(8) {
          
          // 收藏按钮
          Button()
            .padding(8)
            .backgroundColor(Color.Transparent)
            .onClick(() => {
              // 收藏逻辑
            }) {
              Text(product.isFavorite ? '❤️' : '🤍')
                .fontSize(20);
            }
          
          // 加入购物车按钮
          Button()
            .backgroundColor('#3b82f6')
            .padding({ top: 8, bottom: 8, left: 16, right: 16 })
            .borderRadius(6)
            .onClick(() => {
              // 加购逻辑
            }) {
              Text('🛒')
                .fontSize(16)
                .fontColor('#ffffff');
            }
        }
      }
    }
  })
}
(3)筛选面板
// RN 筛选面板
{showFilters && (
  <View style={styles.filterPanel}>
    {/* 筛选内容 */}
  </View>
)}

// 鸿蒙筛选面板适配
if (this.showFilters) {
  Stack()
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .zIndex(1000) {
    
    Column()
      .width('100%')
      .justifyContent(FlexAlign.End) {
      
      // 筛选面板内容
      Column()
        .width('100%')
        .backgroundColor('#ffffff')
        .borderTopLeftRadius(12)
        .borderTopRightRadius(12) {
        
        // 面板头部
        Row()
          .justifyContent(FlexAlign.SpaceBetween)
          .alignItems(ItemAlign.Center)
          .padding(16) {
          
          Text('筛选条件')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1e293b');
          
          Button()
            .width(30)
            .height(30)
            .borderRadius(15)
            .backgroundColor('#e2e8f0')
            .onClick(() => {
              this.showFilters = false;
            }) {
              Text('×')
                .fontSize(18)
                .fontColor('#64748b');
            }
        }
        
        // 价格区间筛选
        Column()
          .padding(16)
          .borderBottom({ width: 1, color: '#e2e8f0' }) {
          
          Text('价格区间')
            .fontSize(14)
            .fontColor('#475569')
            .marginBottom(12);
          
          Row()
            .alignItems(ItemAlign.Center) {
            
            TextInput()
              .flexGrow(1)
              .border({ width: 1, color: '#cbd5e1' })
              .borderRadius(6)
              .padding(8)
              .fontSize(14)
              .backgroundColor('#f8fafc')
              .placeholder('最低价')
              .type(InputType.Number)
              .value(this.minPrice)
              .onChange((value) => {
                this.minPrice = value;
              });
            
            Text('-')
              .fontSize(14)
              .fontColor('#64748b')
              .margin({ left: 8, right: 8 });
            
            TextInput()
              .flexGrow(1)
              .border({ width: 1, color: '#cbd5e1' })
              .borderRadius(6)
              .padding(8)
              .fontSize(14)
              .backgroundColor('#f8fafc')
              .placeholder('最高价')
              .type(InputType.Number)
              .value(this.maxPrice)
              .onChange((value) => {
                this.maxPrice = value;
              });
          }
        }
        
        // 品牌筛选
        // ...
        
        // 操作按钮
        Row()
          .padding(16) {
          
          Button()
            .flexGrow(0.45)
            .backgroundColor('#e2e8f0')
            .paddingVertical(12)
            .borderRadius(6)
            .marginRight(8)
            .onClick(() => {
              this.minPrice = '';
              this.maxPrice = '';
              this.selectedBrand = 'all';
            }) {
              Text('重置')
                .fontSize(14)
                .fontColor('#475569')
                .fontWeight(FontWeight.Medium);
            }
          
          Button()
            .flexGrow(0.45)
            .backgroundColor('#3b82f6')
            .paddingVertical(12)
            .borderRadius(6)
            .marginLeft(8)
            .onClick(() => {
              this.applyFilters();
            }) {
              Text('应用')
                .fontSize(14)
                .fontColor('#ffffff')
                .fontWeight(FontWeight.Medium);
            }
        }
      }
    }
  }
}

3. 样式

RN 的 StyleSheet 样式在鸿蒙中通过链式样式 + @Styles 装饰器实现复用,核心适配规则:

// RN 样式定义
const styles = StyleSheet.create({
  productCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 12,
    flex: 0.48,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    position: 'relative',
  },
});

// 鸿蒙样式适配
@Entry
@Component
struct SearchApp {
  // 通用商品卡片样式封装
  @Styles productCardStyle() {
    .backgroundColor('#ffffff')
    .borderRadius(12)
    .padding(12)
    .flexGrow(0.48)
    .shadow({ color: '#000', offsetX: 0, offsetY: 1, opacity: 0.1, radius: 2 });
  }
  
  build() {
    Column() {
      // 使用通用样式
      List() {
        ForEach(this.searchResults, (product: Product) => {
          ListItem() {
            Column()
              .productCardStyle() {
                // 商品卡片内容...
              }
          }
        })
      }
    }
  }
}

适配亮点:

  • 基础样式无缝迁移:颜色、间距、圆角、字体大小等属性直接复用,仅调整语法格式;
  • 布局属性对齐flex: 0.48flexGrow(0.48)flexDirection: 'row'flexDirection(FlexDirection.Row)
  • 阴影效果统一:鸿蒙的 shadow 配置替代 RN 的 elevation/shadow 双配置,一次配置兼顾所有场景;
  • 样式复用增强:通过 @Styles 装饰器封装通用样式,提升代码复用率。
  • 数据层完全复用:商品、搜索历史等核心数据模型字段完全一致,仅调整类型定义语法,保证两端数据逻辑统一;
  • 业务逻辑对等实现:搜索、筛选、排序、历史管理等核心业务逻辑,仅需适配平台特有 API(如弹窗、键盘事件),业务逻辑零修改;
  • 布局架构镜像还原:保持“搜索栏 + 动态内容区 + 筛选面板 + 底部导航”的核心布局结构,保证用户体验的一致性;
  • 视觉体验统一:复用相同的色值、间距、圆角、字体体系,保证两端视觉效果无差异;
  • 性能优化差异化:RN 端优化 FlatListgetItemLayout/initialNumToRender 等属性,鸿蒙端优化 ListcachedCount/lanes 等属性,充分利用各平台的原生性能优势。

这份 React Native 搜索页的跨端适配实践,验证了 ArkTS 与 React 技术体系的高度兼容性。对于搜索页这类交互复杂、数据驱动的场景,核心的业务逻辑、数据模型、布局架构均可实现高效复用,仅需适配平台特有 API 和性能优化策略,是跨端电商应用开发的高效路径。


真实演示案例代码:







// App.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, FlatList, TextInput } from 'react-native';

// Base64 图标库
const ICONS_BASE64 = {
  search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  filter: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  sort: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  cart: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  home: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  history: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  favorite: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  close: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};

const { width, height } = Dimensions.get('window');

// 商品类型
type Product = {
  id: string;
  name: string;
  price: number;
  originalPrice?: number;
  rating: number;
  reviewCount: number;
  store: string;
  category: string;
  imageUrl?: string;
  isFavorite: boolean;
  discount?: number;
  tags?: string[];
};

// 搜索历史类型
type SearchHistory = {
  id: string;
  query: string;
  timestamp: Date;
};

// 关键词搜索商品应用组件
const SearchApp: React.FC = () => {
  const [products] = useState<Product[]>([
    { id: '1', name: 'iPhone 15 Pro Max', price: 9999, originalPrice: 10999, rating: 4.8, reviewCount: 1250, store: '苹果官方旗舰店', category: '手机', isFavorite: false, discount: 10, tags: ['热销', '正品'] },
    { id: '2', name: '小米13 Ultra', price: 5999, originalPrice: 6499, rating: 4.7, reviewCount: 890, store: '小米官方旗舰店', category: '手机', isFavorite: true, discount: 8, tags: ['新品', '优惠'] },
    { id: '3', name: '戴尔XPS 13', price: 8999, originalPrice: 9999, rating: 4.6, reviewCount: 560, store: '戴尔官方店', category: '电脑', isFavorite: false, discount: 10, tags: ['热销'] },
    { id: '4', name: '索尼WH-1000XM5', price: 2499, originalPrice: 2999, rating: 4.9, reviewCount: 2100, store: '索尼专卖店', category: '耳机', isFavorite: false, discount: 17, tags: ['热销', '推荐'] },
    { id: '5', name: '美的变频空调', price: 3299, originalPrice: 3999, rating: 4.5, reviewCount: 780, store: '美的官方旗舰店', category: '家电', isFavorite: true, discount: 18, tags: ['爆款'] },
    { id: '6', name: '耐克运动鞋', price: 799, originalPrice: 899, rating: 4.4, reviewCount: 3200, store: 'Nike官方店', category: '服饰', isFavorite: false, discount: 11, tags: ['热销', '时尚'] },
    { id: '7', name: 'iPad Air', price: 4599, originalPrice: 4999, rating: 4.7, reviewCount: 920, store: '苹果官方旗舰店', category: '平板', isFavorite: false, discount: 8, tags: ['推荐'] },
    { id: '8', name: '海尔双开门冰箱', price: 4299, originalPrice: 4999, rating: 4.6, reviewCount: 640, store: '海尔官方店', category: '家电', isFavorite: true, discount: 14, tags: ['节能', '静音'] },
  ]);

  const [searchQuery, setSearchQuery] = useState<string>('');
  const [searchResults, setSearchResults] = useState<Product[]>([]);
  const [searchHistories, setSearchHistories] = useState<SearchHistory[]>([
    { id: 'h1', query: 'iPhone', timestamp: new Date(Date.now() - 86400000) },
    { id: 'h2', query: '笔记本电脑', timestamp: new Date(Date.now() - 172800000) },
    { id: 'h3', query: '运动鞋', timestamp: new Date(Date.now() - 259200000) },
    { id: 'h4', query: '耳机', timestamp: new Date(Date.now() - 345600000) },
    { id: 'h5', query: '空调', timestamp: new Date(Date.now() - 432000000) },
  ]);
  const [showFilters, setShowFilters] = useState<boolean>(false);
  const [sortBy, setSortBy] = useState<string>('relevance');
  const [minPrice, setMinPrice] = useState<string>('');
  const [maxPrice, setMaxPrice] = useState<string>('');
  const [selectedBrand, setSelectedBrand] = useState<string>('all');

  const toggleFavorite = (productId: string) => {
    Alert.alert('收藏', `商品已${products.find(p => p.id === productId)?.isFavorite ? '取消收藏' : '收藏'}`);
  };

  const addToCart = (productId: string) => {
    Alert.alert('加入购物车', `已将商品加入购物车`);
  };

  const viewProduct = (productId: string) => {
    Alert.alert('商品详情', `查看商品: ${products.find(p => p.id === productId)?.name}`);
  };

  const performSearch = () => {
    if (searchQuery.trim() === '') {
      setSearchResults([]);
      return;
    }

    // 添加到搜索历史
    const newHistory: SearchHistory = {
      id: `h${Date.now()}`,
      query: searchQuery,
      timestamp: new Date()
    };
    setSearchHistories(prev => [newHistory, ...prev.slice(0, 9)]);

    // 模拟搜索结果
    const results = products.filter(product => 
      product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
      product.category.toLowerCase().includes(searchQuery.toLowerCase())
    );
    setSearchResults(results);
  };

  const clearSearchHistory = () => {
    setSearchHistories([]);
    Alert.alert('清除成功', '搜索历史已清除');
  };

  const removeSearchHistory = (id: string) => {
    setSearchHistories(prev => prev.filter(history => history.id !== id));
  };

  const applyFilters = () => {
    let filtered = [...searchResults];
    
    // 按价格过滤
    if (minPrice) {
      filtered = filtered.filter(p => p.price >= parseInt(minPrice));
    }
    if (maxPrice) {
      filtered = filtered.filter(p => p.price <= parseInt(maxPrice));
    }
    
    // 按品牌过滤
    if (selectedBrand !== 'all') {
      filtered = filtered.filter(p => p.store.includes(selectedBrand));
    }
    
    // 按排序规则排序
    filtered.sort((a, b) => {
      if (sortBy === 'price-low') return a.price - b.price;
      if (sortBy === 'price-high') return b.price - a.price;
      if (sortBy === 'rating') return b.rating - a.rating;
      return 0; // 默认相关性排序
    });
    
    setSearchResults(filtered);
    setShowFilters(false);
  };

  const renderProduct = ({ item }: { item: Product }) => (
    <TouchableOpacity style={styles.productCard} onPress={() => viewProduct(item.id)}>
      <View style={styles.productImage}>
        <Text style={styles.productImageText}>📱</Text>
      </View>
      <Text style={styles.productName} numberOfLines={2}>{item.name}</Text>
      <View style={styles.priceContainer}>
        <Text style={styles.currentPrice}>¥{item.price}</Text>
        {item.originalPrice && (
          <Text style={styles.originalPrice}>¥{item.originalPrice}</Text>
        )}
        {item.discount && (
          <Text style={styles.discountBadge}>-{item.discount}%</Text>
        )}
      </View>
      <View style={styles.productRating}>
        <Text style={styles.ratingText}>{item.rating}</Text>
        <Text style={styles.reviewCount}>({item.reviewCount})</Text>
      </View>
      <Text style={styles.storeName}>{item.store}</Text>
      <View style={styles.productTags}>
        {item.tags?.map(tag => (
          <View key={tag} style={styles.tag}>
            <Text style={styles.tagText}>{tag}</Text>
          </View>
        ))}
      </View>
      <View style={styles.productActions}>
        <TouchableOpacity 
          style={styles.favoriteButton} 
          onPress={() => toggleFavorite(item.id)}
        >
          <Text style={styles.favoriteIcon}>{item.isFavorite ? '❤️' : '🤍'}</Text>
        </TouchableOpacity>
        <TouchableOpacity 
          style={styles.cartButton} 
          onPress={() => addToCart(item.id)}
        >
          <Text style={styles.cartIcon}>🛒</Text>
        </TouchableOpacity>
      </View>
    </TouchableOpacity>
  );

  return (
    <SafeAreaView style={styles.container}>
      {/* 头部搜索栏 */}
      <View style={styles.header}>
        <View style={styles.searchContainer}>
          <Text style={styles.searchIcon}>🔍</Text>
          <TextInput
            style={styles.searchInput}
            placeholder="搜索商品..."
            value={searchQuery}
            onChangeText={setSearchQuery}
            onSubmitEditing={performSearch}
            returnKeyType="search"
          />
          {searchQuery.length > 0 && (
            <TouchableOpacity 
              style={styles.clearButton}
              onPress={() => setSearchQuery('')}
            >
              <Text style={styles.clearButtonText}>×</Text>
            </TouchableOpacity>
          )}
        </View>
        <TouchableOpacity 
          style={styles.searchButton}
          onPress={performSearch}
        >
          <Text style={styles.searchButtonText}>搜索</Text>
        </TouchableOpacity>
      </View>

      <ScrollView style={styles.content}>
        {/* 搜索建议区域 */}
        {searchQuery === '' && (
          <View style={styles.suggestionSection}>
            {/* 热门搜索 */}
            <Text style={styles.sectionTitle}>热门搜索</Text>
            <View style={styles.popularSearches}>
              {['手机', '笔记本', '耳机', '运动鞋', '空调', '护肤品'].map((term, index) => (
                <TouchableOpacity 
                  key={index}
                  style={styles.popularTag}
                  onPress={() => {
                    setSearchQuery(term);
                    performSearch();
                  }}
                >
                  <Text style={styles.popularTagText}>{term}</Text>
                </TouchableOpacity>
              ))}
            </View>

            {/* 搜索历史 */}
            {searchHistories.length > 0 && (
              <>
                <View style={styles.historyHeader}>
                  <Text style={styles.sectionTitle}>搜索历史</Text>
                  <TouchableOpacity onPress={clearSearchHistory}>
                    <Text style={styles.clearHistoryText}>清空</Text>
                  </TouchableOpacity>
                </View>
                <View style={styles.historyList}>
                  {searchHistories.map(history => (
                    <View key={history.id} style={styles.historyItem}>
                      <TouchableOpacity 
                        style={styles.historyItemContent}
                        onPress={() => {
                          setSearchQuery(history.query);
                          performSearch();
                        }}
                      >
                        <Text style={styles.historyText}>{history.query}</Text>
                      </TouchableOpacity>
                      <TouchableOpacity 
                        onPress={() => removeSearchHistory(history.id)}
                        style={styles.removeHistoryButton}
                      >
                        <Text style={styles.removeHistoryText}>×</Text>
                      </TouchableOpacity>
                    </View>
                  ))}
                </View>
              </>
            )}
          </View>
        )}

        {/* 搜索结果区域 */}
        {searchResults.length > 0 && (
          <View style={styles.resultSection}>
            <View style={styles.resultHeader}>
              <Text style={styles.resultCount}>
                找到 {searchResults.length} 件商品
              </Text>
              <View style={styles.resultActions}>
                <TouchableOpacity 
                  style={styles.filterButton}
                  onPress={() => setShowFilters(true)}
                >
                  <Text style={styles.filterButtonText}>筛选</Text>
                </TouchableOpacity>
                <TouchableOpacity 
                  style={styles.sortButton}
                  onPress={() => {
                    const options = ['relevance', 'price-low', 'price-high', 'rating'];
                    const currentIndex = options.indexOf(sortBy);
                    const nextIndex = (currentIndex + 1) % options.length;
                    setSortBy(options[nextIndex]);
                  }}
                >
                  <Text style={styles.sortButtonText}>排序</Text>
                </TouchableOpacity>
              </View>
            </View>

            <FlatList
              data={searchResults}
              renderItem={renderProduct}
              keyExtractor={item => item.id}
              numColumns={2}
              columnWrapperStyle={styles.productRow}
              showsVerticalScrollIndicator={false}
              contentContainerStyle={styles.productList}
            />
          </View>
        )}

        {/* 空搜索结果 */}
        {searchQuery !== '' && searchResults.length === 0 && (
          <View style={styles.emptyResult}>
            <Text style={styles.emptyResultText}>没有找到"{searchQuery}"相关的商品</Text>
            <Text style={styles.suggestionText}>试试其他关键词或查看热门搜索</Text>
          </View>
        )}
      </ScrollView>

      {/* 筛选面板 */}
      {showFilters && (
        <View style={styles.filterPanel}>
          <View style={styles.filterPanelHeader}>
            <Text style={styles.filterPanelTitle}>筛选条件</Text>
            <TouchableOpacity 
              style={styles.closeFilterButton}
              onPress={() => setShowFilters(false)}
            >
              <Text style={styles.closeFilterText}>×</Text>
            </TouchableOpacity>
          </View>
          
          <View style={styles.filterOption}>
            <Text style={styles.filterOptionText}>价格区间</Text>
            <View style={styles.priceRange}>
              <TextInput
                style={styles.priceInput}
                placeholder="最低价"
                keyboardType="numeric"
                value={minPrice}
                onChangeText={setMinPrice}
              />
              <Text style={styles.toText}>-</Text>
              <TextInput
                style={styles.priceInput}
                placeholder="最高价"
                keyboardType="numeric"
                value={maxPrice}
                onChangeText={setMaxPrice}
              />
            </View>
          </View>
          
          <View style={styles.filterOption}>
            <Text style={styles.filterOptionText}>品牌</Text>
            <View style={styles.brandOptions}>
              {['all', '苹果', '小米', '戴尔', '索尼', '美的', 'Nike', '海尔'].map(brand => (
                <TouchableOpacity 
                  key={brand}
                  style={[
                    styles.brandButton, 
                    selectedBrand === brand && styles.selectedBrandButton
                  ]}
                  onPress={() => setSelectedBrand(brand)}
                >
                  <Text style={[
                    styles.brandButtonText,
                    selectedBrand === brand && styles.selectedBrandButtonText
                  ]}>
                    {brand === 'all' ? '全部' : brand}
                  </Text>
                </TouchableOpacity>
              ))}
            </View>
          </View>
          
          <View style={styles.filterActions}>
            <TouchableOpacity 
              style={styles.resetButton}
              onPress={() => {
                setMinPrice('');
                setMaxPrice('');
                setSelectedBrand('all');
              }}
            >
              <Text style={styles.resetButtonText}>重置</Text>
            </TouchableOpacity>
            <TouchableOpacity 
              style={styles.applyButton}
              onPress={applyFilters}
            >
              <Text style={styles.applyButtonText}>应用</Text>
            </TouchableOpacity>
          </View>
        </View>
      )}

      {/* 底部导航 */}
      <View style={styles.bottomNav}>
        <TouchableOpacity style={styles.navItem}>
          <Text style={styles.navIcon}>🏠</Text>
          <Text style={styles.navText}>首页</Text>
        </TouchableOpacity>
        <TouchableOpacity style={[styles.navItem, styles.activeNavItem]}>
          <Text style={styles.navIcon}>🔍</Text>
          <Text style={styles.navText}>搜索</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.navItem}>
          <Text style={styles.navIcon}>🛒</Text>
          <Text style={styles.navText}>购物车</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.navItem}>
          <Text style={styles.navIcon}>👤</Text>
          <Text style={styles.navText}>我的</Text>
        </TouchableOpacity>
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f7fa',
  },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    backgroundColor: '#ffffff',
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  searchContainer: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f1f5f9',
    borderRadius: 20,
    paddingVertical: 10,
    paddingHorizontal: 16,
    marginRight: 10,
  },
  searchIcon: {
    fontSize: 18,
    color: '#64748b',
    marginRight: 8,
  },
  searchInput: {
    flex: 1,
    fontSize: 14,
    color: '#1e293b',
  },
  clearButton: {
    width: 20,
    height: 20,
    borderRadius: 10,
    backgroundColor: '#cbd5e1',
    alignItems: 'center',
    justifyContent: 'center',
  },
  clearButtonText: {
    fontSize: 14,
    color: '#64748b',
    fontWeight: 'bold',
  },
  searchButton: {
    backgroundColor: '#3b82f6',
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 6,
  },
  searchButtonText: {
    color: '#ffffff',
    fontSize: 14,
    fontWeight: '500',
  },
  content: {
    flex: 1,
    padding: 16,
  },
  suggestionSection: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  popularSearches: {
    flexDirection: 'row',
    flexWrap: 'wrap',
  },
  popularTag: {
    backgroundColor: '#f1f5f9',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 16,
    marginRight: 8,
    marginBottom: 8,
  },
  popularTagText: {
    fontSize: 12,
    color: '#475569',
  },
  historyHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginTop: 16,
  },
  clearHistoryText: {
    fontSize: 12,
    color: '#3b82f6',
  },
  historyList: {
    marginTop: 8,
  },
  historyItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 10,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  historyItemContent: {
    flex: 1,
  },
  historyText: {
    fontSize: 14,
    color: '#1e293b',
  },
  removeHistoryButton: {
    width: 20,
    height: 20,
    borderRadius: 10,
    backgroundColor: '#e2e8f0',
    alignItems: 'center',
    justifyContent: 'center',
  },
  removeHistoryText: {
    fontSize: 12,
    color: '#64748b',
  },
  resultSection: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  resultHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 16,
  },
  resultCount: {
    fontSize: 14,
    color: '#64748b',
  },
  resultActions: {
    flexDirection: 'row',
  },
  filterButton: {
    backgroundColor: '#e2e8f0',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 6,
    marginRight: 8,
  },
  filterButtonText: {
    fontSize: 12,
    color: '#475569',
  },
  sortButton: {
    backgroundColor: '#e2e8f0',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 6,
  },
  sortButtonText: {
    fontSize: 12,
    color: '#475569',
  },
  productList: {
    paddingBottom: 80,
  },
  productRow: {
    justifyContent: 'space-between',
    marginBottom: 16,
  },
  productCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 12,
    flex: 0.48,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    position: 'relative',
  },
  productImage: {
    width: '100%',
    height: 120,
    borderRadius: 8,
    backgroundColor: '#f1f5f9',
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: 8,
  },
  productImageText: {
    fontSize: 30,
  },
  productName: {
    fontSize: 14,
    fontWeight: '500',
    color: '#1e293b',
    marginBottom: 8,
  },
  priceContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 4,
  },
  currentPrice: {
    fontSize: 16,
    color: '#ef4444',
    fontWeight: 'bold',
    marginRight: 8,
  },
  originalPrice: {
    fontSize: 12,
    color: '#94a3b8',
    textDecorationLine: 'line-through',
    marginRight: 8,
  },
  discountBadge: {
    fontSize: 10,
    color: '#ffffff',
    backgroundColor: '#ef4444',
    paddingHorizontal: 4,
    paddingVertical: 2,
    borderRadius: 4,
  },
  productRating: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 4,
  },
  ratingText: {
    fontSize: 12,
    color: '#f59e0b',
    marginRight: 8,
  },
  reviewCount: {
    fontSize: 12,
    color: '#64748b',
  },
  storeName: {
    fontSize: 12,
    color: '#64748b',
    marginBottom: 8,
  },
  productTags: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    marginBottom: 8,
  },
  tag: {
    backgroundColor: '#dbeafe',
    paddingHorizontal: 6,
    paddingVertical: 2,
    borderRadius: 4,
    marginRight: 4,
    marginBottom: 4,
  },
  tagText: {
    fontSize: 10,
    color: '#3b82f6',
  },
  productActions: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginTop: 8,
  },
  favoriteButton: {
    padding: 8,
  },
  favoriteIcon: {
    fontSize: 20,
  },
  cartButton: {
    backgroundColor: '#3b82f6',
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 6,
    alignItems: 'center',
  },
  cartIcon: {
    fontSize: 16,
    color: '#ffffff',
  },
  emptyResult: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    paddingVertical: 40,
  },
  emptyResultText: {
    fontSize: 16,
    color: '#64748b',
    marginBottom: 8,
  },
  suggestionText: {
    fontSize: 14,
    color: '#94a3b8',
  },
  filterPanel: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0,0,0,0.5)',
    zIndex: 1000,
    justifyContent: 'flex-end',
  },
  filterPanelHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    backgroundColor: '#ffffff',
    padding: 16,
    borderTopLeftRadius: 12,
    borderTopRightRadius: 12,
  },
  filterPanelTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#1e293b',
  },
  closeFilterButton: {
    width: 30,
    height: 30,
    borderRadius: 15,
    backgroundColor: '#e2e8f0',
    alignItems: 'center',
    justifyContent: 'center',
  },
  closeFilterText: {
    fontSize: 18,
    color: '#64748b',
  },
  filterOption: {
    backgroundColor: '#ffffff',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  filterOptionText: {
    fontSize: 14,
    color: '#475569',
    marginBottom: 12,
  },
  priceRange: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  priceInput: {
    flex: 1,
    borderWidth: 1,
    borderColor: '#cbd5e1',
    borderRadius: 6,
    padding: 8,
    fontSize: 14,
    backgroundColor: '#f8fafc',
  },
  toText: {
    fontSize: 14,
    color: '#64748b',
    marginHorizontal: 8,
  },
  brandOptions: {
    flexDirection: 'row',
    flexWrap: 'wrap',
  },
  brandButton: {
    backgroundColor: '#f1f5f9',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 16,
    marginRight: 8,
    marginBottom: 8,
  },
  selectedBrandButton: {
    backgroundColor: '#3b82f6',
  },
  brandButtonText: {
    fontSize: 12,
    color: '#475569',
  },
  selectedBrandButtonText: {
    color: '#ffffff',
  },
  filterActions: {
    flexDirection: 'row',
    backgroundColor: '#ffffff',
    padding: 16,
  },
  resetButton: {
    flex: 0.45,
    backgroundColor: '#e2e8f0',
    paddingVertical: 12,
    borderRadius: 6,
    alignItems: 'center',
    marginRight: 8,
  },
  resetButtonText: {
    fontSize: 14,
    color: '#475569',
    fontWeight: '500',
  },
  applyButton: {
    flex: 0.45,
    backgroundColor: '#3b82f6',
    paddingVertical: 12,
    borderRadius: 6,
    alignItems: 'center',
    marginLeft: 8,
  },
  applyButtonText: {
    fontSize: 14,
    color: '#ffffff',
    fontWeight: '500',
  },
  bottomNav: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    backgroundColor: '#ffffff',
    borderTopWidth: 1,
    borderTopColor: '#e2e8f0',
    paddingVertical: 12,
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
  },
  navItem: {
    alignItems: 'center',
    flex: 1,
  },
  activeNavItem: {
    paddingTop: 4,
    borderTopWidth: 2,
    borderTopColor: '#3b82f6',
  },
  navIcon: {
    fontSize: 20,
    color: '#94a3b8',
    marginBottom: 4,
  },
  activeNavIcon: {
    color: '#3b82f6',
  },
  navText: {
    fontSize: 12,
    color: '#94a3b8',
  },
  activeNavText: {
    color: '#3b82f6',
    fontWeight: '500',
  },
});

export default SearchApp;



请添加图片描述


打包

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

在这里插入图片描述

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

在这里插入图片描述

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

请添加图片描述
本文探讨了基于React Native实现的商品搜索系统架构设计及跨端适配策略。系统采用React Hooks进行轻量级状态管理,构建了包含搜索查询、结果、历史记录等多层次状态模型,并实现了完整的搜索流程和筛选排序功能。技术实现上使用React Native核心组件确保跨平台兼容性,采用Base64编码图标解决资源兼容问题,通过Dimensions API适配不同屏幕尺寸。文章还针对鸿蒙系统的API兼容性问题提出了测试建议,并提供了性能优化、错误处理和可访问性改进方案。该设计兼顾了用户体验、开发效率和跨平台适配需求。

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐