该垃圾分类答题应用采用了清晰的模块化架构,通过 TypeScript 接口定义了完整的数据模型体系。核心数据模型包括:

  • GarbageType 枚举类型定义了四大垃圾类别
  • GarbageItem 描述具体垃圾物品的属性
  • QuizQuestion 构建答题挑战的题目结构
  • AnswerRecord 记录用户答题历史

这种强类型设计为跨端开发提供了坚实的基础,确保了数据结构在 React Native 和 HarmonyOS 平台上的一致性。在跨端开发实践中,建议将数据模型单独抽离为独立模块,便于在不同平台间共享和维护。

状态管理

应用采用 React Hooks 进行状态管理,核心状态包括:

  • 垃圾物品列表和题目数据(静态数据)
  • 当前题目索引、用户选择答案和答题结果(动态交互状态)
  • 分数统计和答题记录(业务逻辑状态)

在 HarmonyOS 跨端开发中,React Hooks 可以映射到 ArkUI 的状态管理机制:

  • useState 对应 ArkUI 的 @State 装饰器
  • useEffect 对应 ArkUI 的 @Watch 装饰器
  • useCallbackuseMemo 可以通过 ArkUI 的 @Memo 装饰器实现类似功能

应用中的 handleAnswerSelect 函数实现了答题逻辑的核心处理,包括答案验证、分数更新和记录保存。这种集中式的业务逻辑设计便于跨端迁移,但需要注意异步操作在不同平台上的处理差异。

UI 组件

应用使用 React Native 核心组件构建用户界面,主要包括:

  • FlatList 用于高效渲染列表数据
  • TouchableOpacity 实现可点击的选项按钮
  • Alert 用于展示答题结果和挑战完成信息
  • StyleSheet 实现样式与逻辑分离

在 HarmonyOS 跨端适配中,这些组件可以映射到对应的 ArkUI 组件:

  1. 列表渲染:React Native 的 FlatList 对应 ArkUI 的 List 组件,两者都支持虚拟列表优化,但在 API 设计上存在差异。例如,FlatListrenderItem 属性在 ArkUI 中对应 ListItem 组件。

  2. 触摸交互TouchableOpacity 对应 ArkUI 的 Button 组件或 Gesture 手势组件。需要注意的是,ArkUI 的 Button 组件默认带有样式,可能需要额外配置才能实现与 React Native 一致的视觉效果。

  3. 弹窗提示Alert.alert 在 HarmonyOS 中对应 dialog.showAlertDialog API,需要进行适配封装。

  4. 样式系统:React Native 的 StyleSheet 与 ArkUI 的样式系统在语法上存在差异,但核心概念(如 Flexbox 布局)是一致的。建议使用跨端样式解决方案(如 styled-components 或自定义样式转换工具)来简化样式管理。

交互

应用实现了流畅的答题交互流程,包括:

  • 实时的答案选择反馈
  • 清晰的正误结果提示
  • 平滑的题目切换动画
  • 完整的得分统计和挑战完成反馈

在跨端开发中,用户体验的一致性是关键。需要注意:

  1. 触摸反馈:不同平台的触摸反馈机制存在差异,建议使用统一的视觉反馈设计(如颜色变化、缩放效果)来确保用户体验的一致性。

  2. 动画效果:React Native 的 Animated API 在 HarmonyOS 中可以通过 ArkUI 的 animateTo 方法实现类似功能,但需要注意性能优化和平台差异。

  3. 响应式布局:应用使用 Dimensions.get('window') 获取屏幕尺寸,在 HarmonyOS 中可以通过 window.getWindowProperties API 实现。建议使用相对单位和弹性布局,减少对固定尺寸的依赖。

性能优化与跨端考量

应用在性能优化方面采用了以下策略:

  • 使用 FlatList 实现高效的列表渲染
  • 避免不必要的状态更新和组件重渲染
  • 合理使用条件渲染优化 UI 显示

在 HarmonyOS 跨端开发中,还需要注意:

  1. 内存管理:HarmonyOS 对内存管理有严格要求,建议避免使用过多的闭包和匿名函数,及时清理不再使用的资源。

  2. 异步操作:React Native 的异步操作(如 setTimeoutfetch)在 HarmonyOS 中需要使用对应的 API(如 setTimeout@ohos.net.http),建议封装统一的异步工具函数。

  3. 图片资源:应用中使用了 base64 编码的图标,在生产环境中建议使用图片资源文件,并针对不同平台进行优化。

数据模型

type GarbageType = '可回收物' | '有害垃圾' | '湿垃圾' | '干垃圾';

type QuizQuestion = {
  id: string;
  itemName: string;
  correctAnswer: GarbageType;
  options: GarbageType[];
  explanation: string;
};

type AnswerRecord = {
  questionId: string;
  userAnswer: GarbageType;
  isCorrect: boolean;
  timestamp: string;
};

这种数据结构设计体现了答题系统的多层级关系:

  1. 题库结构:题目、答案、选项和解析
  2. 答题记录:用户答案和正确性标记
  3. 时间维度:答题时间的完整记录

在鸿蒙ArkUI中,可以使用教育数据类:

// 鸿蒙答题数据模型
@Observed
class QuizQuestionHarmony {
  id: string = '';
  itemName: string = '';
  correctAnswer: GarbageType = '可回收物';
  options: GarbageType[] = [];
  explanation: string = '';
  
  get formattedOptions(): string[] {
    return this.options.map(option => this.getOptionDisplayName(option));
  }
  
  private getOptionDisplayName(option: GarbageType): string {
    switch(option) {
      case '可回收物': return '♻️ 可回收物';
      case '有害垃圾': return '⚠️ 有害垃圾'; 
      case '湿垃圾': return '?? 湿垃圾';
      case '干垃圾': return '🗑️ 干垃圾';
      default: return option;
    }
  }
}

// 鸿蒙答题记录模型
@Observed
class AnswerRecordHarmony {
  questionId: string = '';
  userAnswer: GarbageType = '可回收物';
  isCorrect: boolean = false;
  timestamp: string = '';
  
  get formattedTime(): string {
    return new Date(this.timestamp).toLocaleTimeString();
  }
}

答题状态

const [currentQuestionIndex, setCurrentQuestionIndex] = useState<number>(0);
const [selectedAnswer, setSelectedAnswer] = useState<GarbageType | null>(null);
const [showResult, setShowResult] = useState<boolean>(false);
const [score, setScore] = useState<number>(0);
const [answerRecords, setAnswerRecords] = useState<AnswerRecord[]>([]);

状态管理实现了完整的答题流程:

  1. 题目索引:当前题目位置跟踪
  2. 答案选择:用户选择的答案状态
  3. 结果显示:答题结果的展示控制
  4. 分数统计:实时成绩计算
  5. 记录跟踪:完整的答题历史

答案处理

const handleAnswerSelect = (answer: GarbageType) => {
  setSelectedAnswer(answer);
  const currentQuestion = quizQuestions[currentQuestionIndex];
  const isCorrect = answer === currentQuestion.correctAnswer;
  
  // 更新分数
  if (isCorrect) {
    setScore(prev => prev + 1);
  }
  
  // 记录答题
  const record: AnswerRecord = {
    questionId: currentQuestion.id,
    userAnswer: answer,
    isCorrect,
    timestamp: new Date().toISOString(),
  };
  
  setAnswerRecords(prev => [...prev, record]);
  setShowResult(true);
};

答题处理实现了智能的业务逻辑:

  1. 答案验证:即时比对正确答案
  2. 分数更新:实时成绩计算
  3. 记录保存:完整的答题历史记录
  4. 状态切换:自动进入结果展示状态

状态

const renderOption = (option: GarbageType) => {
  const currentQuestion = quizQuestions[currentQuestionIndex];
  const isSelected = selectedAnswer === option;
  let optionStyle = styles.optionButton;
  let textStyle = styles.optionText;
  
  if (showResult) {
    if (option === currentQuestion.correctAnswer) {
      optionStyle = [styles.optionButton, styles.correctOption];
      textStyle = styles.correctOptionText;
    } else if (isSelected && option !== currentQuestion.correctAnswer) {
      optionStyle = [styles.optionButton, styles.incorrectOption];
      textStyle = styles.incorrectOptionText;
    }
  }
  // ...
};

选项设计采用了多状态反馈:

  1. 默认状态:中性颜色等待选择
  2. 选中状态:突出显示当前选择
  3. 正确状态:绿色标记正确答案
  4. 错误状态:红色标记错误选择

分类指南的图标化展示

<View style={styles.categoryItem}>
  <Text style={styles.categoryIcon}>♻️</Text>
  <Text style={styles.categoryName}>可回收物</Text>
  <Text style={styles.categoryDesc}>废纸、塑料、金属、玻璃、织物</Text>
</View>

分类指南采用了直观的视觉编码:

  1. 图标语义:使用emoji直观表示分类
  2. 名称标识:明确的分类名称
  3. 示例说明:具体的物品示例

鸿蒙跨端适配:教育游戏的技术实现

答题服务的跨平台策略

// 统一答题服务接口
interface QuizService {
  getQuestions(): Promise<QuizQuestion[]>;
  submitAnswer(record: AnswerRecord): Promise<boolean>;
  getQuizStats(): Promise<QuizStats>;
  resetQuiz(): Promise<void>;
}

// 鸿蒙实现
class HarmonyQuizService implements QuizService {
  async submitAnswer(record: AnswerRecord): Promise<boolean> {
    try {
      const database = await relationalStore.getRdbStore();
      const valueBucket = new relationalStore.ValuesBucket();
      valueBucket.putString('questionId', record.questionId);
      valueBucket.putString('userAnswer', record.userAnswer);
      valueBucket.putBoolean('isCorrect', record.isCorrect);
      valueBucket.putString('timestamp', record.timestamp);
      
      await database.insert('answer_records', valueBucket);
      return true;
    } catch (error) {
      console.error('保存答题记录失败:', error);
      return false;
    }
  }
}


--

-在环保类移动应用的开发迭代中,“交互式答题+分类知识科普”的组合模式成为提升用户环保认知的核心载体——从单纯的知识展示,升级为“答题挑战+即时反馈+知识巩固”的闭环体验,既贴合用户的学习习惯,也为跨端开发提出了“状态驱动答题流程、动态样式渲染、跨端交互体验一致性”的新挑战。本文以 React Native 开发的垃圾分类答题应用为例,深度拆解其“答题状态机”设计、动态样式渲染逻辑,并系统阐述向鸿蒙(HarmonyOS)ArkTS 跨端迁移的技术路径,聚焦“答题状态分层管理、动态样式条件渲染、跨端交互语义等价”三大核心维度,为环保类应用的跨端迭代提供可落地的技术参考。

React Native 端核心架构与实现逻辑

环保场景下的类型系统与状态模型设计

该应用基于垃圾分类的业务特性构建了强类型的状态模型体系,是跨端开发中“数据层复用”的核心基础,完全贴合垃圾分类答题的实际场景:

  • 首先定义核心业务类型:GarbageType 联合类型限定垃圾分类的四大核心类别(可回收物/有害垃圾/湿垃圾/干垃圾),作为整个应用的类型基石;GarbageItem 类型定义垃圾物品的核心属性,包含名称、分类、描述等基础信息;QuizQuestion 类型构建答题挑战的题目模型,包含题干(itemName)、正确答案、选项列表、解析等答题场景必备字段;AnswerRecord 类型记录用户答题行为,关联题目 ID、用户答案、正确性、时间戳,形成完整的答题行为闭环。
  • 状态管理层面,采用 React Hooks 构建“答题流程状态机”:currentQuestionIndex 控制当前答题进度,selectedAnswer 记录用户当前选择的答案,showResult 控制答题结果展示状态,score 实时计算答题得分,answerRecords 存储完整答题记录。这种分层的状态设计,将答题流程拆解为“选题-作答-结果展示-下一题”四个阶段,每个阶段通过独立状态变量控制,避免了状态耦合导致的逻辑混乱。
  • 初始数据层面,garbageItems 存储常见垃圾物品信息,quizQuestions 构建结构化的答题题库,所有初始数据均基于预定义的类型系统,通过 TypeScript 强类型约束保证数据的准确性,为跨端迁移提供了“数据结构 100% 复用”的基础。

答题流程的状态驱动逻辑设计

应用的核心业务逻辑围绕“答题状态机”展开,所有核心函数均为纯业务逻辑,与 UI 渲染层完全解耦,是跨端迁移中“逻辑复用”的核心:

  • handleAnswerSelect 函数实现答题核心逻辑:接收用户选择的答案后,首先记录选中状态,然后对比正确答案判断答题结果,同步更新得分,并生成答题记录存入 answerRecords,最后触发结果展示状态(showResult = true)。该函数是“作答阶段”的核心,涵盖了答案校验、得分计算、行为记录三大核心能力,无任何 UI 相关代码,可直接复用于鸿蒙端。
  • goToNextQuestion 函数控制答题流程推进:判断当前是否为最后一题,若非最后一题则重置答题状态(清空选中答案、隐藏结果、推进题目索引),若为最后一题则展示最终得分并重置整个答题流程。该函数实现了“结果展示阶段”到“下一题/挑战完成阶段”的流转,是答题流程的核心控制逻辑。
  • resetQuiz 函数实现答题流程重置:将所有答题状态变量恢复初始值,包括题目索引、选中答案、结果展示状态、得分、答题记录,保证用户可重复参与答题挑战,符合环保类应用“反复学习”的业务场景。

这种“状态变量分层+核心逻辑纯函数化”的设计,使得答题业务逻辑与 UI 渲染完全分离,跨端迁移时只需复用核心函数,无需修改业务逻辑,大幅降低了跨端开发的成本。

动态样式的条件渲染实现

垃圾分类答题应用的核心视觉体验在于“答题过程中选项样式的动态变化”——未选择、已选择、答对、答错四种状态对应不同的样式,应用通过条件渲染实现了精细化的样式控制,是跨端适配中“视觉体验一致性”的核心:

  • renderOption 函数是动态样式的核心载体:首先获取当前题目信息和选中状态,然后根据 showResult 状态(是否展示结果)进行样式分支判断:
    • 未展示结果时:仅判断选项是否被选中,选中项应用 selectedOption 样式(蓝色背景+白色文字),未选中项应用默认样式;
    • 展示结果时:优先判断是否为正确答案(应用 correctOption 绿色样式),若为用户选错的选项则应用 incorrectOption 红色样式,其他选项保持默认样式。
  • 样式体系层面,通过 StyleSheet.create 定义了完整的样式变量:基础选项样式(optionButton/optionText)、选中状态样式(selectedOption/selectedOptionText)、正确状态样式(correctOption/correctOptionText)、错误状态样式(incorrectOption/incorrectOptionText),形成“基础样式+状态样式”的双层样式体系,保证了样式的可维护性和可复用性。
  • 垃圾分类展示样式的动态化:在“常见垃圾识别”模块中,通过三元表达式根据垃圾类型动态设置文字颜色(可回收物绿色、有害垃圾红色、湿垃圾橙色、干垃圾灰色),实现了垃圾分类的视觉差异化展示,符合用户对垃圾分类颜色标识的认知习惯。

动态样式的条件渲染完全基于状态变量,无硬编码样式,这种设计使得跨端迁移时只需复用样式判断逻辑,即可保证答题过程中视觉反馈的一致性。

模块化的 UI 组件架构设计

应用采用模块化的组件架构,将界面划分为多个功能模块,每个模块聚焦单一业务场景,符合“单一职责原则”,也为跨端迁移提供了“模块级等价重构”的清晰路径:

  • 头部模块:展示应用标题和实时得分,采用横向布局实现标题与得分的左右分布,得分使用蓝色突出展示,符合答题类应用“实时反馈得分”的用户体验设计原则。
  • 答题信息模块(quizInfoCard):展示答题挑战的说明信息,采用卡片式设计,通过阴影和圆角提升视觉层次感,是环保类应用“知识科普”的基础载体。
  • 题目核心模块(questionCard):整合题目展示、选项列表、结果反馈三大功能,是整个应用的核心 UI 组件:
    • 题目展示区:包含题目序号、题干、待分类物品名称,待分类物品名称采用高亮背景(eff6ff)突出展示,提升用户注意力;
    • 选项列表区:采用“两行两列”的弹性布局(flexDirection: row + flexWrap: wrap + minWidth: 48%),保证选项在不同屏幕尺寸下的适配性,符合移动端答题应用的交互习惯;
    • 结果反馈区:根据 showResult 状态条件渲染,包含答题结果(正确/错误)、解析说明、下一题按钮,解析说明采用行高优化提升可读性,下一题按钮采用蓝色背景突出展示,引导用户操作。
  • 分类指南模块(guideCard):采用网格布局展示四大垃圾分类的图标、名称、说明,每个分类项使用独立卡片,符合“知识模块化展示”的设计原则,图标采用 emoji 符号(♻️/⚠️/🍎/🗑️)增强视觉识别性。
  • 环保小贴士模块(tipsCard):以文本列表形式展示环保建议,通过行高和间距优化提升阅读体验,是环保类应用“知识延伸”的重要模块。
  • 垃圾识别模块(identificationCard):通过 FlatList 展示常见垃圾物品及其分类,分类文字颜色动态变化,实现了“物品-分类-视觉标识”的关联展示,符合用户“快速识别垃圾类型”的核心需求。
  • 底部导航模块:包含首页、分类指南、挑战、我的四个核心入口,选中项通过顶部蓝色边框标识,符合移动端应用的导航设计规范,“挑战”入口采用奖杯图标(🏆),贴合答题挑战的业务场景。

这种模块化的 UI 设计,使得每个模块均可独立迁移至鸿蒙端,大幅提升了跨端开发的效率和可维护性。

从 React Native 到鸿蒙 ArkTS 的跨端适配路径

核心技术体系的等价映射

跨端适配的核心是“类型系统复用、状态逻辑复用、动态样式逻辑复用、UI 组件语义等价重构”,React Native 与鸿蒙 ArkTS 的核心能力可实现精准映射,以下是垃圾分类答题场景关键技术点的等价转换:

React Native 核心能力 鸿蒙 ArkTS 等价实现 垃圾分类答题场景适配要点
TypeScript 类型定义(GarbageType/QuizQuestion 等) TypeScript 类型定义(GarbageType/QuizQuestion 等) 所有业务类型 100% 复用,保证垃圾分类数据结构的一致性
useState 状态管理 @State/@Link 装饰器 答题状态变量(currentQuestionIndex/selectedAnswer 等)等价迁移,初始值完全复用
handleAnswerSelect/goToNextQuestion 核心函数 同名类方法 100% 复用业务逻辑,仅需调整 this 指向,保证答题流程的一致性
FlatList 垃圾识别列表渲染 List + ListItem datalistDatarenderItemitemGenerator,适配长列表渲染场景
条件渲染(showResult 控制结果展示) 条件渲染(if 语句) 完全复用结果展示的条件判断逻辑,保证答题流程的视觉反馈一致
动态样式条件判断(renderOption 中的样式分支) 样式变量条件赋值 复用样式判断逻辑,通过 ArkTS 样式变量实现动态样式切换
TouchableOpacity 选项按钮交互 Button 组件 onPressonClick,设置 backgroundColor: Transparent 去除默认样式,保证选项点击体验一致
弹性布局(flexDirection/flexWrap) Flex 布局(FlexDirection/FlexWrap) 选项列表的两行两列布局等价实现,保证不同屏幕尺寸的适配性

关键模块的迁移实操

类型系统与状态管理迁移

React Native 端的 TypeScript 类型定义可直接复制到鸿蒙 ArkTS 工程,状态变量通过 @State 装饰器实现等价管理,核心业务逻辑函数完全复用:

// React Native 端类型定义(100% 复用)
type GarbageType = '可回收物' | '有害垃圾' | '湿垃圾' | '干垃圾';
type QuizQuestion = {
  id: string;
  itemName: string;
  correctAnswer: GarbageType;
  options: GarbageType[];
  explanation: string;
};

// 鸿蒙 ArkTS 端状态管理
@Entry
@Component
struct WasteClassificationQuiz {
  // 等价于 React Native 的 useState,初始值完全复用
  @State currentQuestionIndex: number = 0;
  @State selectedAnswer: GarbageType | null = null;
  @State showResult: boolean = false;
  @State score: number = 0;
  @State answerRecords: AnswerRecord[] = [];
  
  // 初始题库数据(100% 复用 React Native 端数据)
  @State quizQuestions: QuizQuestion[] = [
    { 
      id: '1', 
      itemName: '旧报纸', 
      correctAnswer: '可回收物', 
      options: ['可回收物', '有害垃圾', '湿垃圾', '干垃圾'],
      explanation: '干净的纸质材料属于可回收物,应投入蓝色垃圾桶'
    },
    // 其他题目数据...
  ];
  
  // 核心答题逻辑函数(100% 复用,仅调整 this 指向)
  handleAnswerSelect(answer: GarbageType) {
    this.selectedAnswer = answer;
    const currentQuestion = this.quizQuestions[this.currentQuestionIndex];
    const isCorrect = answer === currentQuestion.correctAnswer;
    
    if (isCorrect) {
      this.score += 1;
    }
    
    const record: AnswerRecord = {
      questionId: currentQuestion.id,
      userAnswer: answer,
      isCorrect,
      timestamp: new Date().toISOString(),
    };
    
    this.answerRecords.push(record);
    this.showResult = true;
  }
  
  // 答题流程推进函数(完全复用)
  goToNextQuestion() {
    if (this.currentQuestionIndex < this.quizQuestions.length - 1) {
      this.currentQuestionIndex += 1;
      this.selectedAnswer = null;
      this.showResult = false;
    } else {
      AlertDialog.show({
        title: '挑战完成!',
        message: `你的得分是 ${this.score}/${this.quizQuestions.length}`,
        confirm: {
          value: '确定',
          action: () => this.resetQuiz()
        }
      });
    }
  }
  
  // 重置答题函数(完全复用)
  resetQuiz() {
    this.currentQuestionIndex = 0;
    this.selectedAnswer = null;
    this.showResult = false;
    this.score = 0;
    this.answerRecords = [];
  }
  
  // 组件构建逻辑...
}

可以看到,除了状态定义方式(@State 替代 useState)和弹窗 API(AlertDialog 替代 Alert)的调整,核心的类型定义、初始数据、业务逻辑函数均完全复用,保证了跨端答题流程的一致性。

动态选项样式的等价实现

React Native 端的 renderOption 动态样式逻辑在鸿蒙端通过“样式变量条件赋值”实现,保证答题过程中选项样式的视觉一致性:

// 鸿蒙 ArkTS 端选项渲染函数
@Builder
renderOption(option: GarbageType) {
  const currentQuestion = this.quizQuestions[this.currentQuestionIndex];
  const isSelected = this.selectedAnswer === option;
  
  // 样式变量条件赋值(复用 React Native 端的判断逻辑)
  let optionBgColor = '#f1f5f9';
  let textColor = '#1e293b';
  
  if (this.showResult) {
    if (option === currentQuestion.correctAnswer) {
      optionBgColor = '#10b981'; // 正确答案绿色
      textColor = '#ffffff';
    } else if (isSelected && option !== currentQuestion.correctAnswer) {
      optionBgColor = '#ef4444'; // 错误选择红色
      textColor = '#ffffff';
    }
  } else if (isSelected) {
    optionBgColor = '#3b82f6'; // 选中状态蓝色
    textColor = '#ffffff';
  }
  
  // 选项按钮实现(等价于 TouchableOpacity)
  Button() {
    Text(option)
      .fontSize(14)
      .color(textColor)
  }
  .backgroundColor(optionBgColor)
  .padding(12)
  .borderRadius(8)
  .minWidth('48%')
  .marginVertical(4)
  .enabled(!this.showResult) // 展示结果后禁用按钮
  .onClick(() => this.handleAnswerSelect(option));
}

该实现完全复用了 React Native 端的样式判断逻辑,通过动态设置 optionBgColortextColor 实现样式切换,Button 组件的 enabled 属性控制答题结果展示后禁用选项点击,与 React Native 端的 !showResult && handleAnswerSelect 逻辑等价,保证了答题交互体验的一致性。

核心答题模块的等价重构

React Native 端的 questionCard 模块是核心,鸿蒙端通过 Column/Row 布局实现等价重构,保证答题核心体验的一致性:

// 鸿蒙 ArkTS 端答题核心模块
@Builder
renderQuestionCard() {
  const currentQuestion = this.quizQuestions[this.currentQuestionIndex];
  
  Column()
    .backgroundColor('#ffffff')
    .borderRadius(12)
    .padding(16)
    .marginBottom(16)
    .shadow({ radius: 2, color: '#000', opacity: 0.1, offsetX: 0, offsetY: 1 })
  {
    // 题目序号
    Text(`${this.currentQuestionIndex + 1}`)
      .fontSize(14)
      .color('#64748b')
      .marginBottom(8);
    
    // 题干
    Text('请将以下物品分类:')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .color('#1e293b')
      .marginBottom(8);
    
    // 待分类物品名称
    Text(currentQuestion.itemName)
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
      .color('#3b82f6')
      .textAlign(TextAlign.Center)
      .marginVertical(16)
      .padding(12)
      .backgroundColor('#eff6ff')
      .borderRadius(8);
    
    // 选项列表(两行两列布局,等价于 flexWrap)
    Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween })
      .marginBottom(16)
    {
      ForEach(currentQuestion.options, (option: GarbageType) => {
        this.renderOption(option);
      });
    }
    
    // 结果展示区域(条件渲染,等价于 showResult && ...)
    if (this.showResult) {
      Column()
        .marginTop(16)
        .padding(12)
        .backgroundColor('#f8fafc')
        .borderRadius(8)
      {
        // 答题结果
        const lastRecord = this.answerRecords[this.answerRecords.length - 1];
        Text(lastRecord.isCorrect ? '✓ 回答正确!' : '✗ 回答错误!')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .color(lastRecord.isCorrect ? '#10b981' : '#ef4444')
          .marginBottom(8);
        
        // 解析说明
        Text(currentQuestion.explanation)
          .fontSize(14)
          .color('#64748b')
          .marginBottom(12)
          .lineHeight(20);
        
        // 下一题按钮
        Button(this.currentQuestionIndex < this.quizQuestions.length - 1 ? '下一题' : '完成挑战')
          .backgroundColor('#3b82f6')
          .padding(12)
          .borderRadius(8)
          .onClick(() => this.goToNextQuestion())
        {
          Text(this.currentQuestionIndex < this.quizQuestions.length - 1 ? '下一题' : '完成挑战')
            .color('#ffffff')
            .fontWeight(FontWeight.Medium);
        }
      }
    }
  }
}

该实现完全复刻了 React Native 端的答题核心模块结构:题目序号、题干、待分类物品名称、选项列表、结果展示区域一一对应;布局上通过 Flex 组件的 wrap: FlexWrap.Wrap 实现选项的两行两列布局,与 React Native 端的 flexWrap: wrap 等价;结果展示区域通过 if (this.showResult) 实现条件渲染,与 React Native 端的 showResult && (...) 逻辑一致,保证了跨端答题核心体验的一致性。

垃圾识别列表的等价实现

React Native 端的 FlatList 垃圾识别列表在鸿蒙端通过 List 组件实现,动态颜色逻辑完全复用:

// 鸿蒙 ArkTS 端垃圾识别列表
@Builder
renderIdentificationList() {
  Column()
    .backgroundColor('#ffffff')
    .borderRadius(12)
    .padding(16)
    .marginBottom(16)
    .shadow({ radius: 2, color: '#000', opacity: 0.1, offsetX: 0, offsetY: 1 })
  {
    Text('常见垃圾识别')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .color('#1e293b')
      .marginBottom(12);
    
    List() {
      ForEach(this.garbageItems.slice(0, 4), (item: GarbageItem) => {
        ListItem() {
          Row()
            .justifyContent(FlexAlign.SpaceBetween)
            .paddingVertical(8)
            .borderBottom({ width: 1, color: '#e2e8f0' })
          {
            Text(item.name)
              .fontSize(14)
              .color('#1e293b');
            
            // 动态颜色逻辑(100% 复用 React Native 端)
            let typeColor = '#6b7280'; // 默认干垃圾颜色
            if (item.type === '可回收物') typeColor = '#10b981';
            else if (item.type === '有害垃圾') typeColor = '#ef4444';
            else if (item.type === '湿垃圾') typeColor = '#f59e0b';
            
            Text(item.type)
              .fontSize(14)
              .fontWeight(FontWeight.Medium)
              .color(typeColor);
          }
        }
      });
    }
    .showsScrollbar(false); // 等价于 showsVerticalScrollIndicator={false}
  }
}

该实现复用了 React Native 端的垃圾类型动态颜色逻辑,通过 List + ListItem 实现长列表渲染,showsScrollbar(false) 隐藏滚动条,与 React Native 端的 showsVerticalScrollIndicator={false} 等价,保证了垃圾识别列表的视觉和交互体验一致。

跨端开发的垃圾分类场景最佳实践

1. 类型系统与业务逻辑的最大化复用

GarbageType/QuizQuestion/AnswerRecord 等类型定义、handleAnswerSelect/goToNextQuestion 等核心业务函数抽离为独立的 TS 模块,React Native 与鸿蒙工程共享该模块,保证垃圾分类答题核心逻辑的 100% 复用。对于答题流程的状态判断逻辑(如是否为最后一题、答案是否正确),封装为工具类方法,两端统一调用,避免重复开发。

2. 动态样式逻辑的等价转换

跨端适配中,动态样式的核心是“判断逻辑复用+样式属性等价映射”:

  • 答题选项的样式判断逻辑(未选择/已选择/答对/答错)完全复用,仅需将 React Native 的 StyleSheet 样式对象转换为鸿蒙的样式属性(如 backgroundColor/color);
  • 垃圾类型的动态颜色逻辑直接复用,保证垃圾分类的视觉标识一致性;
  • 状态驱动的样式变化(如结果展示后的选项样式)通过相同的条件判断实现,保证答题过程中视觉反馈的一致性。
3. 交互体验的语义等价实现

垃圾分类答题应用的核心交互体验包括“选项点击、结果反馈、流程推进”,跨端适配时需保证交互语义的等价:

  • 选项点击:React Native 的 TouchableOpacity 对应鸿蒙的 Button 组件,通过设置透明背景去除默认样式,保证点击区域和交互反馈一致;
  • 结果反馈:两端采用相同的视觉标识(绿色正确/红色错误),解析说明的排版样式(行高、间距)保持一致;
  • 流程推进:下一题按钮的位置、样式、点击逻辑完全一致,挑战完成后的弹窗提示内容保持一致;
  • 长列表交互:React Native 的 FlatList 对应鸿蒙的 List 组件,均采用懒加载策略,保证垃圾识别列表的滑动体验一致。
4. 布局适配的跨端一致性

垃圾分类答题应用包含多种布局场景(选项的两行两列、分类指南的网格布局、垃圾识别的列表布局),跨端适配时需保证布局的等价:

  • 弹性布局:React Native 的 flexDirection/flexWrap 对应鸿蒙的 FlexDirection/FlexWrap,参数值完全复用;
  • 网格布局:分类指南的“两行两列”布局,两端均采用“宽度 48% + 间距”的实现方式;
  • 卡片布局:所有卡片组件的圆角(12px)、内边距(16px)、阴影效果保持一致,保证视觉体验的统一性;
  • 响应式适配:利用两端的尺寸 API(React Native 的 Dimensions / 鸿蒙的 vp 单位)实现屏幕适配,保证不同设备上的展示效果一致。

总结

关键点回顾

  1. React Native 端的垃圾分类答题应用构建了“类型系统+状态机+纯业务逻辑+动态样式”的四层架构,将答题流程拆解为独立的状态阶段,核心业务逻辑与 UI 渲染完全解耦,为跨端复用奠定了基础;
  2. 鸿蒙跨端适配的核心是“类型系统 100% 复用、业务逻辑 100% 复用、动态样式逻辑复用、UI 组件语义等价重构”,重点保证答题核心流程、选项动态样式、垃圾分类视觉标识的一致性;
  3. 环保类应用跨端迭代需遵循“语义等价而非代码等价”的原则,聚焦“核心业务逻辑复用、交互体验一致、视觉标识统一”三大核心,可大幅降低迭代成本,保证多平台环保应用的体验统一。

该垃圾分类答题应用的跨端实践验证了 React Native 与鸿蒙 ArkTS 在环保类应用迭代开发中的适配可行性,核心业务逻辑复用率可达 90% 以上。通过统一的类型系统、等价的状态管理、一致的业务逻辑,可快速完成跨端迁移,为环保类应用的跨端迭代提供了可复制的技术路径。



真实演示案例代码:


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

// Base64 图标库
const ICONS_BASE64 = {
  recycle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  quiz: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  trash: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  tips: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  achievement: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  settings: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  home: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};

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

// 垃圾类型
type GarbageType = '可回收物' | '有害垃圾' | '湿垃圾' | '干垃圾';

// 垃圾物品类型
type GarbageItem = {
  id: string;
  name: string;
  type: GarbageType;
  description: string;
  image: string;
};

// 答题挑战题目类型
type QuizQuestion = {
  id: string;
  itemName: string;
  correctAnswer: GarbageType;
  options: GarbageType[];
  explanation: string;
};

// 用户答题记录类型
type AnswerRecord = {
  questionId: string;
  userAnswer: GarbageType;
  isCorrect: boolean;
  timestamp: string;
};

const WasteClassificationQuizApp: React.FC = () => {
  const [garbageItems] = useState<GarbageItem[]>([
    { id: '1', name: '塑料瓶', type: '可回收物', description: '清洁后的塑料瓶可以回收利用', image: '' },
    { id: '2', name: '电池', type: '有害垃圾', description: '含有重金属,需特殊处理', image: '' },
    { id: '3', name: '剩菜剩饭', type: '湿垃圾', description: '易腐烂的生物质废弃物', image: '' },
    { id: '4', name: '纸巾', type: '干垃圾', description: '污染后无法回收的纸制品', image: '' },
    { id: '5', name: '玻璃瓶', type: '可回收物', description: '透明玻璃瓶可回收利用', image: '' },
    { id: '6', name: '过期药品', type: '有害垃圾', description: '对环境和人体有害', image: '' },
    { id: '7', name: '苹果核', type: '湿垃圾', description: '厨余垃圾,可生物降解', image: '' },
    { id: '8', name: '陶瓷碎片', type: '干垃圾', description: '不可回收的无机废物', image: '' },
  ]);

  const [quizQuestions] = useState<QuizQuestion[]>([
    { 
      id: '1', 
      itemName: '旧报纸', 
      correctAnswer: '可回收物', 
      options: ['可回收物', '有害垃圾', '湿垃圾', '干垃圾'],
      explanation: '干净的纸质材料属于可回收物,应投入蓝色垃圾桶'
    },
    { 
      id: '2', 
      itemName: '杀虫剂罐', 
      correctAnswer: '有害垃圾', 
      options: ['可回收物', '有害垃圾', '湿垃圾', '干垃圾'],
      explanation: '装过化学物质的容器属于有害垃圾,应投入红色垃圾桶'
    },
    { 
      id: '3', 
      itemName: '鱼骨头', 
      correctAnswer: '湿垃圾', 
      options: ['可回收物', '有害垃圾', '湿垃圾', '干垃圾'],
      explanation: '动物骨骼属于厨余垃圾,应投入棕色垃圾桶'
    },
    { 
      id: '4', 
      itemName: '一次性餐具', 
      correctAnswer: '干垃圾', 
      options: ['可回收物', '有害垃圾', '湿垃圾', '干垃圾'],
      explanation: '被污染且不易清洗的塑料制品属于干垃圾,应投入黑色垃圾桶'
    },
    { 
      id: '5', 
      itemName: '易拉罐', 
      correctAnswer: '可回收物', 
      options: ['可回收物', '有害垃圾', '湿垃圾', '干垃圾'],
      explanation: '金属容器属于可回收物,应投入蓝色垃圾桶'
    },
  ]);

  const [currentQuestionIndex, setCurrentQuestionIndex] = useState<number>(0);
  const [selectedAnswer, setSelectedAnswer] = useState<GarbageType | null>(null);
  const [showResult, setShowResult] = useState<boolean>(false);
  const [score, setScore] = useState<number>(0);
  const [answerRecords, setAnswerRecords] = useState<AnswerRecord[]>([]);

  // 处理答案选择
  const handleAnswerSelect = (answer: GarbageType) => {
    setSelectedAnswer(answer);
    const currentQuestion = quizQuestions[currentQuestionIndex];
    const isCorrect = answer === currentQuestion.correctAnswer;
    
    // 更新分数
    if (isCorrect) {
      setScore(prev => prev + 1);
    }
    
    // 记录答题
    const record: AnswerRecord = {
      questionId: currentQuestion.id,
      userAnswer: answer,
      isCorrect,
      timestamp: new Date().toISOString(),
    };
    
    setAnswerRecords(prev => [...prev, record]);
    setShowResult(true);
  };

  // 下一题
  const goToNextQuestion = () => {
    if (currentQuestionIndex < quizQuestions.length - 1) {
      setCurrentQuestionIndex(prev => prev + 1);
      setSelectedAnswer(null);
      setShowResult(false);
    } else {
      Alert.alert('挑战完成!', `你的得分是 ${score}/${quizQuestions.length}`);
      resetQuiz();
    }
  };

  // 重置挑战
  const resetQuiz = () => {
    setCurrentQuestionIndex(0);
    setSelectedAnswer(null);
    setShowResult(false);
    setScore(0);
    setAnswerRecords([]);
  };

  // 渲染题目选项
  const renderOption = (option: GarbageType) => {
    const currentQuestion = quizQuestions[currentQuestionIndex];
    const isSelected = selectedAnswer === option;
    let optionStyle = styles.optionButton;
    let textStyle = styles.optionText;
    
    if (showResult) {
      if (option === currentQuestion.correctAnswer) {
        optionStyle = [styles.optionButton, styles.correctOption];
        textStyle = styles.correctOptionText;
      } else if (isSelected && option !== currentQuestion.correctAnswer) {
        optionStyle = [styles.optionButton, styles.incorrectOption];
        textStyle = styles.incorrectOptionText;
      }
    } else if (isSelected) {
      optionStyle = [styles.optionButton, styles.selectedOption];
      textStyle = styles.selectedOptionText;
    }
    
    return (
      <TouchableOpacity 
        key={option}
        style={optionStyle}
        onPress={() => !showResult && handleAnswerSelect(option)}
      >
        <Text style={textStyle}>{option}</Text>
      </TouchableOpacity>
    );
  };

  return (
    <SafeAreaView style={styles.container}>
      {/* 头部 */}
      <View style={styles.header}>
        <Text style={styles.title}>垃圾分类挑战</Text>
        <Text style={styles.score}>得分: {score}/{quizQuestions.length}</Text>
      </View>

      <ScrollView style={styles.content}>
        {/* 挑战说明 */}
        <View style={styles.quizInfoCard}>
          <Text style={styles.quizInfoTitle}>垃圾分类小挑战</Text>
          <Text style={styles.quizInfoText}>将以下物品正确分类,测试你的环保知识!</Text>
        </View>

        {/* 当前题目 */}
        <View style={styles.questionCard}>
          <Text style={styles.questionNumber}>{currentQuestionIndex + 1}</Text>
          <Text style={styles.questionText}>请将以下物品分类:</Text>
          <Text style={styles.itemName}>{quizQuestions[currentQuestionIndex]?.itemName}</Text>
          
          {/* 选项 */}
          <View style={styles.optionsContainer}>
            {quizQuestions[currentQuestionIndex]?.options.map(renderOption)}
          </View>
          
          {/* 结果展示 */}
          {showResult && (
            <View style={styles.resultContainer}>
              <Text style={[
                styles.resultText, 
                answerRecords[answerRecords.length - 1]?.isCorrect 
                  ? styles.correctText 
                  : styles.incorrectText
              ]}>
                {answerRecords[answerRecords.length - 1]?.isCorrect ? '✓ 回答正确!' : '✗ 回答错误!'}
              </Text>
              <Text style={styles.explanationText}>
                {quizQuestions[currentQuestionIndex]?.explanation}
              </Text>
              <TouchableOpacity 
                style={styles.nextButton}
                onPress={goToNextQuestion}
              >
                <Text style={styles.nextButtonText}>
                  {currentQuestionIndex < quizQuestions.length - 1 ? '下一题' : '完成挑战'}
                </Text>
              </TouchableOpacity>
            </View>
          )}
        </View>

        {/* 垃圾分类指南 */}
        <View style={styles.guideCard}>
          <Text style={styles.guideTitle}>垃圾分类指南</Text>
          <View style={styles.categoryContainer}>
            <View style={styles.categoryItem}>
              <Text style={styles.categoryIcon}>♻️</Text>
              <Text style={styles.categoryName}>可回收物</Text>
              <Text style={styles.categoryDesc}>废纸、塑料、金属、玻璃、织物</Text>
            </View>
            <View style={styles.categoryItem}>
              <Text style={styles.categoryIcon}>⚠️</Text>
              <Text style={styles.categoryName}>有害垃圾</Text>
              <Text style={styles.categoryDesc}>电池、灯管、药品、油漆</Text>
            </View>
            <View style={styles.categoryItem}>
              <Text style={styles.categoryIcon}>🍎</Text>
              <Text style={styles.categoryName}>湿垃圾</Text>
              <Text style={styles.categoryDesc}>食材废料、剩菜剩饭、花卉绿植</Text>
            </View>
            <View style={styles.categoryItem}>
              <Text style={styles.categoryIcon}>🗑️</Text>
              <Text style={styles.categoryName}>干垃圾</Text>
              <Text style={styles.categoryDesc}>餐巾纸、烟蒂、破损陶瓷、猫砂</Text>
            </View>
          </View>
        </View>

        {/* 环保小贴士 */}
        <View style={styles.tipsCard}>
          <Text style={styles.tipsTitle}>环保小贴士</Text>
          <Text style={styles.tipsText}>• 减少使用一次性用品</Text>
          <Text style={styles.tipsText}>• 优先选择可回收包装</Text>
          <Text style={styles.tipsText}>• 厨余垃圾沥干水分再投放</Text>
          <Text style={styles.tipsText}>• 有害垃圾单独存放</Text>
          <Text style={styles.tipsText}>• 积极参与社区回收活动</Text>
        </View>

        {/* 垃圾识别 */}
        <View style={styles.identificationCard}>
          <Text style={styles.identificationTitle}>常见垃圾识别</Text>
          <FlatList
            data={garbageItems.slice(0, 4)}
            renderItem={({ item }) => (
              <View style={styles.identificationItem}>
                <Text style={styles.identificationName}>{item.name}</Text>
                <Text style={[
                  styles.identificationType,
                  { color: item.type === '可回收物' ? '#10b981' : 
                          item.type === '有害垃圾' ? '#ef4444' : 
                          item.type === '湿垃圾' ? '#f59e0b' : '#6b7280' }
                ]}>
                  {item.type}
                </Text>
              </View>
            )}
            keyExtractor={item => item.id}
            showsVerticalScrollIndicator={false}
          />
        </View>

        {/* 开始挑战按钮 */}
        <TouchableOpacity 
          style={styles.startChallengeButton}
          onPress={resetQuiz}
        >
          <Text style={styles.startChallengeButtonText}>重新开始挑战</Text>
        </TouchableOpacity>
      </ScrollView>

      {/* 底部导航 */}
      <View style={styles.bottomNav}>
        <TouchableOpacity 
          style={[styles.navItem, styles.activeNavItem]} 
          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>
        
        <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',
  },
  score: {
    fontSize: 16,
    color: '#3b82f6',
    fontWeight: '500',
  },
  content: {
    flex: 1,
    padding: 16,
  },
  quizInfoCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  quizInfoTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 8,
  },
  quizInfoText: {
    fontSize: 14,
    color: '#64748b',
  },
  questionCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  questionNumber: {
    fontSize: 14,
    color: '#64748b',
    marginBottom: 8,
  },
  questionText: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 8,
  },
  itemName: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#3b82f6',
    textAlign: 'center',
    marginVertical: 16,
    padding: 12,
    backgroundColor: '#eff6ff',
    borderRadius: 8,
  },
  optionsContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'space-between',
    marginBottom: 16,
  },
  optionButton: {
    backgroundColor: '#f1f5f9',
    padding: 12,
    borderRadius: 8,
    minWidth: '48%',
    marginVertical: 4,
    alignItems: 'center',
  },
  optionText: {
    fontSize: 14,
    color: '#1e293b',
  },
  selectedOption: {
    backgroundColor: '#3b82f6',
  },
  selectedOptionText: {
    color: '#ffffff',
  },
  correctOption: {
    backgroundColor: '#10b981',
  },
  correctOptionText: {
    color: '#ffffff',
  },
  incorrectOption: {
    backgroundColor: '#ef4444',
  },
  incorrectOptionText: {
    color: '#ffffff',
  },
  resultContainer: {
    marginTop: 16,
    padding: 12,
    backgroundColor: '#f8fafc',
    borderRadius: 8,
  },
  resultText: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  correctText: {
    color: '#10b981',
  },
  incorrectText: {
    color: '#ef4444',
  },
  explanationText: {
    fontSize: 14,
    color: '#64748b',
    marginBottom: 12,
    lineHeight: 20,
  },
  nextButton: {
    backgroundColor: '#3b82f6',
    padding: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  nextButtonText: {
    color: '#ffffff',
    fontWeight: '500',
  },
  guideCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  guideTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  categoryContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'space-between',
  },
  categoryItem: {
    width: '48%',
    marginBottom: 12,
    padding: 12,
    backgroundColor: '#f8fafc',
    borderRadius: 8,
  },
  categoryIcon: {
    fontSize: 24,
    textAlign: 'center',
    marginBottom: 4,
  },
  categoryName: {
    fontSize: 14,
    fontWeight: 'bold',
    color: '#1e293b',
    textAlign: 'center',
    marginBottom: 4,
  },
  categoryDesc: {
    fontSize: 12,
    color: '#64748b',
    textAlign: 'center',
  },
  tipsCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  tipsTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  tipsText: {
    fontSize: 14,
    color: '#64748b',
    lineHeight: 22,
    marginBottom: 8,
  },
  identificationCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  identificationTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  identificationItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  identificationName: {
    fontSize: 14,
    color: '#1e293b',
  },
  identificationType: {
    fontSize: 14,
    fontWeight: '500',
  },
  startChallengeButton: {
    backgroundColor: '#3b82f6',
    padding: 16,
    borderRadius: 12,
    alignItems: 'center',
    marginBottom: 16,
  },
  startChallengeButtonText: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: '500',
  },
  bottomNav: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    backgroundColor: '#ffffff',
    borderTopWidth: 1,
    borderTopColor: '#e2e8f0',
    paddingVertical: 12,
  },
  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',
  },
});

export default WasteClassificationQuizApp;

请添加图片描述


打包

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

在这里插入图片描述

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

在这里插入图片描述

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

请添加图片描述
本文介绍了一款垃圾分类答题应用的跨平台开发实践,重点阐述了其数据模型设计、状态管理和UI组件实现。应用采用TypeScript定义核心数据结构,包括题目、答案和记录模型,确保多平台一致性。状态管理使用React Hooks处理答题流程和分数统计,并详细说明了如何在HarmonyOS平台上进行等效实现。UI组件部分对比了React Native与ArkUI的组件映射关系,强调了交互设计和性能优化策略。文章还提供了关键代码片段,展示答题逻辑和状态管理的具体实现,为类似跨平台教育应用的开发提供了参考方案。

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

Logo

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

更多推荐