React Native鸿蒙跨平台如何实现一个垃圾分类答题应用
本文介绍了一款垃圾分类答题应用的跨平台开发实践,重点阐述了其数据模型设计、状态管理和UI组件实现。应用采用TypeScript定义核心数据结构,包括题目、答案和记录模型,确保多平台一致性。状态管理使用React Hooks处理答题流程和分数统计,并详细说明了如何在HarmonyOS平台上进行等效实现。UI组件部分对比了React Native与ArkUI的组件映射关系,强调了交互设计和性能优化策略
该垃圾分类答题应用采用了清晰的模块化架构,通过 TypeScript 接口定义了完整的数据模型体系。核心数据模型包括:
GarbageType枚举类型定义了四大垃圾类别GarbageItem描述具体垃圾物品的属性QuizQuestion构建答题挑战的题目结构AnswerRecord记录用户答题历史
这种强类型设计为跨端开发提供了坚实的基础,确保了数据结构在 React Native 和 HarmonyOS 平台上的一致性。在跨端开发实践中,建议将数据模型单独抽离为独立模块,便于在不同平台间共享和维护。
状态管理
应用采用 React Hooks 进行状态管理,核心状态包括:
- 垃圾物品列表和题目数据(静态数据)
- 当前题目索引、用户选择答案和答题结果(动态交互状态)
- 分数统计和答题记录(业务逻辑状态)
在 HarmonyOS 跨端开发中,React Hooks 可以映射到 ArkUI 的状态管理机制:
useState对应 ArkUI 的@State装饰器useEffect对应 ArkUI 的@Watch装饰器useCallback和useMemo可以通过 ArkUI 的@Memo装饰器实现类似功能
应用中的 handleAnswerSelect 函数实现了答题逻辑的核心处理,包括答案验证、分数更新和记录保存。这种集中式的业务逻辑设计便于跨端迁移,但需要注意异步操作在不同平台上的处理差异。
UI 组件
应用使用 React Native 核心组件构建用户界面,主要包括:
FlatList用于高效渲染列表数据TouchableOpacity实现可点击的选项按钮Alert用于展示答题结果和挑战完成信息StyleSheet实现样式与逻辑分离
在 HarmonyOS 跨端适配中,这些组件可以映射到对应的 ArkUI 组件:
-
列表渲染:React Native 的
FlatList对应 ArkUI 的List组件,两者都支持虚拟列表优化,但在 API 设计上存在差异。例如,FlatList的renderItem属性在 ArkUI 中对应ListItem组件。 -
触摸交互:
TouchableOpacity对应 ArkUI 的Button组件或Gesture手势组件。需要注意的是,ArkUI 的Button组件默认带有样式,可能需要额外配置才能实现与 React Native 一致的视觉效果。 -
弹窗提示:
Alert.alert在 HarmonyOS 中对应dialog.showAlertDialogAPI,需要进行适配封装。 -
样式系统:React Native 的
StyleSheet与 ArkUI 的样式系统在语法上存在差异,但核心概念(如 Flexbox 布局)是一致的。建议使用跨端样式解决方案(如styled-components或自定义样式转换工具)来简化样式管理。
交互
应用实现了流畅的答题交互流程,包括:
- 实时的答案选择反馈
- 清晰的正误结果提示
- 平滑的题目切换动画
- 完整的得分统计和挑战完成反馈
在跨端开发中,用户体验的一致性是关键。需要注意:
-
触摸反馈:不同平台的触摸反馈机制存在差异,建议使用统一的视觉反馈设计(如颜色变化、缩放效果)来确保用户体验的一致性。
-
动画效果:React Native 的
AnimatedAPI 在 HarmonyOS 中可以通过 ArkUI 的animateTo方法实现类似功能,但需要注意性能优化和平台差异。 -
响应式布局:应用使用
Dimensions.get('window')获取屏幕尺寸,在 HarmonyOS 中可以通过window.getWindowPropertiesAPI 实现。建议使用相对单位和弹性布局,减少对固定尺寸的依赖。
性能优化与跨端考量
应用在性能优化方面采用了以下策略:
- 使用
FlatList实现高效的列表渲染 - 避免不必要的状态更新和组件重渲染
- 合理使用条件渲染优化 UI 显示
在 HarmonyOS 跨端开发中,还需要注意:
-
内存管理:HarmonyOS 对内存管理有严格要求,建议避免使用过多的闭包和匿名函数,及时清理不再使用的资源。
-
异步操作:React Native 的异步操作(如
setTimeout、fetch)在 HarmonyOS 中需要使用对应的 API(如setTimeout、@ohos.net.http),建议封装统一的异步工具函数。 -
图片资源:应用中使用了 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;
};
这种数据结构设计体现了答题系统的多层级关系:
- 题库结构:题目、答案、选项和解析
- 答题记录:用户答案和正确性标记
- 时间维度:答题时间的完整记录
在鸿蒙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[]>([]);
状态管理实现了完整的答题流程:
- 题目索引:当前题目位置跟踪
- 答案选择:用户选择的答案状态
- 结果显示:答题结果的展示控制
- 分数统计:实时成绩计算
- 记录跟踪:完整的答题历史
答案处理
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 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;
}
}
// ...
};
选项设计采用了多状态反馈:
- 默认状态:中性颜色等待选择
- 选中状态:突出显示当前选择
- 正确状态:绿色标记正确答案
- 错误状态:红色标记错误选择
分类指南的图标化展示
<View style={styles.categoryItem}>
<Text style={styles.categoryIcon}>♻️</Text>
<Text style={styles.categoryName}>可回收物</Text>
<Text style={styles.categoryDesc}>废纸、塑料、金属、玻璃、织物</Text>
</View>
分类指南采用了直观的视觉编码:
- 图标语义:使用emoji直观表示分类
- 名称标识:明确的分类名称
- 示例说明:具体的物品示例
鸿蒙跨端适配:教育游戏的技术实现
答题服务的跨平台策略
// 统一答题服务接口
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 |
data → listData,renderItem → itemGenerator,适配长列表渲染场景 |
| 条件渲染(showResult 控制结果展示) | 条件渲染(if 语句) | 完全复用结果展示的条件判断逻辑,保证答题流程的视觉反馈一致 |
| 动态样式条件判断(renderOption 中的样式分支) | 样式变量条件赋值 | 复用样式判断逻辑,通过 ArkTS 样式变量实现动态样式切换 |
TouchableOpacity 选项按钮交互 |
Button 组件 |
onPress → onClick,设置 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 端的样式判断逻辑,通过动态设置 optionBgColor 和 textColor 实现样式切换,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 单位)实现屏幕适配,保证不同设备上的展示效果一致。
总结
关键点回顾
- React Native 端的垃圾分类答题应用构建了“类型系统+状态机+纯业务逻辑+动态样式”的四层架构,将答题流程拆解为独立的状态阶段,核心业务逻辑与 UI 渲染完全解耦,为跨端复用奠定了基础;
- 鸿蒙跨端适配的核心是“类型系统 100% 复用、业务逻辑 100% 复用、动态样式逻辑复用、UI 组件语义等价重构”,重点保证答题核心流程、选项动态样式、垃圾分类视觉标识的一致性;
- 环保类应用跨端迭代需遵循“语义等价而非代码等价”的原则,聚焦“核心业务逻辑复用、交互体验一致、视觉标识统一”三大核心,可大幅降低迭代成本,保证多平台环保应用的体验统一。
该垃圾分类答题应用的跨端实践验证了 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的组件映射关系,强调了交互设计和性能优化策略。文章还提供了关键代码片段,展示答题逻辑和状态管理的具体实现,为类似跨平台教育应用的开发提供了参考方案。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐



所有评论(0)