rn_for_openharmony狗狗之家app实战-品种测试实现
摘要(149字): 本文介绍了"狗狗之家"App中的品种测试功能设计与实现。该功能采用游戏化设计,通过"看图识狗"等问答形式提升用户粘性。核心实现包括:1) 题目数据结构设计,包含图片、正确答案和干扰项;2) 利用API数据动态生成随机题目;3) 答题流程的状态管理(计分、进度跟踪等);4) 交互式UI实现,包括题目卡片渲染和选项状态反馈(正确/错误高亮)。
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_dogimg
让学习变得有趣
狗狗之家不只是一个信息展示的 App,还想让用户在使用过程中学到东西。品种测试就是这样一个趣味功能,通过问答的形式测试用户对狗狗品种的了解程度。
答对了有成就感,答错了能学到新知识。这种游戏化的设计能提高用户粘性,让 App 更有意思。
这篇文章会讲解品种测试的设计思路和实现方案,涉及到题目生成、答题流程、计分系统等内容。
当前页面的基础结构
先看现有的代码框架:
import React from 'react';
import {View, StyleSheet} from 'react-native';
import {Header, Empty} from '../../components';
export function BreedQuizPage() {
return (
<View style={s.container}>
<Header title="品种测试" />
<Empty icon="🎯" title="品种测试" desc="测试你对狗狗品种的了解,功能开发中..." />
</View>
);
}
用 Empty 组件做占位,图标用了 🎯 表示测试/目标。
样式:
const s = StyleSheet.create({
container: {flex: 1, backgroundColor: '#f5f5f5'}
});
接下来讲讲如何把它扩展成完整的测试功能。
测试功能的设计
品种测试可以有多种形式:
看图识狗:显示一张狗狗图片,让用户选择是什么品种。
猜寿命:给出品种名,让用户猜测寿命范围。
性格匹配:给出一组性格特点,让用户选择对应的品种。
产地问答:问某个品种来自哪个国家。
我们先实现最直观的"看图识狗"。
题目数据结构
定义题目的类型:
interface Question {
id: number;
image: string;
correctAnswer: string;
options: string[];
}
- id:题目编号
- image:狗狗图片 URL
- correctAnswer:正确答案(品种名)
- options:四个选项
从 API 数据生成题目
品种数据可以用来生成题目:
const generateQuestions = (breeds: Breed[], count: number): Question[] => {
const shuffled = [...breeds].sort(() => Math.random() - 0.5);
const selected = shuffled.slice(0, count);
return selected.map((breed, index) => {
const img = breed.image?.url ||
`https://cdn2.thedogapi.com/images/${breed.reference_image_id}.jpg`;
const wrongAnswers = breeds
.filter(b => b.id !== breed.id)
.sort(() => Math.random() - 0.5)
.slice(0, 3)
.map(b => b.name);
const options = [...wrongAnswers, breed.name]
.sort(() => Math.random() - 0.5);
return {
id: index + 1,
image: img,
correctAnswer: breed.name,
options,
};
});
};
这个函数做了几件事:
打乱品种数组:sort(() => Math.random() - 0.5) 是个简单的随机排序。
选取指定数量:slice(0, count) 取前 N 个作为题目。
生成错误选项:从其他品种里随机选 3 个作为干扰项。
打乱选项顺序:正确答案和错误答案混在一起,再随机排序。
状态设计
测试页面需要管理的状态:
const [loading, setLoading] = useState(true);
const [questions, setQuestions] = useState<Question[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [score, setScore] = useState(0);
const [answered, setAnswered] = useState(false);
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null);
const [finished, setFinished] = useState(false);
逐个解释:
- loading:加载品种数据的状态
- questions:生成的题目数组
- currentIndex:当前题目索引
- score:得分
- answered:当前题是否已作答
- selectedAnswer:用户选择的答案
- finished:是否答完所有题
初始化题目
useEffect(() => {
api.getBreeds().then(breeds => {
const qs = generateQuestions(breeds, 10);
setQuestions(qs);
setLoading(false);
});
}, []);
获取品种数据后生成 10 道题。
当前题目的获取
const currentQuestion = questions[currentIndex];
const progress = `${currentIndex + 1} / ${questions.length}`;
currentQuestion 是当前要显示的题目。
progress 是进度文本,如 “3 / 10”。
答题逻辑
用户点击选项时:
const handleAnswer = (answer: string) => {
if (answered) return;
setSelectedAnswer(answer);
setAnswered(true);
if (answer === currentQuestion.correctAnswer) {
setScore(score + 10);
}
};
防止重复作答:if (answered) return 已经答过就不处理。
记录选择:setSelectedAnswer(answer) 用于显示选中状态。
标记已答:setAnswered(true) 锁定当前题。
计分:答对加 10 分。
下一题逻辑
const handleNext = () => {
if (currentIndex < questions.length - 1) {
setCurrentIndex(currentIndex + 1);
setAnswered(false);
setSelectedAnswer(null);
} else {
setFinished(true);
}
};
如果还有题,进入下一题并重置状态。
如果是最后一题,标记测试结束。
题目卡片的渲染
<Card>
<Text style={s.progress}>{progress}</Text>
<Image source={{uri: currentQuestion.image}} style={s.questionImg} />
<Text style={s.questionText}>这是什么品种?</Text>
</Card>
显示进度、图片、问题文本。
图片样式:
questionImg: {
width: '100%',
height: 200,
borderRadius: 12,
backgroundColor: '#eee',
marginVertical: 16
},
选项的渲染
{currentQuestion.options.map((option, index) => {
const isSelected = selectedAnswer === option;
const isCorrect = option === currentQuestion.correctAnswer;
const showResult = answered;
let optionStyle = s.option;
if (showResult && isCorrect) {
optionStyle = [s.option, s.optionCorrect];
} else if (showResult && isSelected && !isCorrect) {
optionStyle = [s.option, s.optionWrong];
}
return (
<TouchableOpacity
key={index}
style={optionStyle}
onPress={() => handleAnswer(option)}
disabled={answered}
>
<Text style={s.optionText}>{option}</Text>
</TouchableOpacity>
);
})}
选项的样式根据状态变化:
- 未作答:默认样式
- 作答后,正确答案:绿色背景
- 作答后,选错的答案:红色背景
disabled={answered} 作答后禁用点击。
选项样式
option: {
padding: 16,
borderRadius: 10,
backgroundColor: '#f5f5f5',
marginBottom: 10,
},
optionCorrect: {
backgroundColor: '#E8F5E9',
borderWidth: 2,
borderColor: '#4CAF50',
},
optionWrong: {
backgroundColor: '#FFEBEE',
borderWidth: 2,
borderColor: '#F44336',
},
- 默认灰色背景
- 正确是浅绿色 + 绿色边框
- 错误是浅红色 + 红色边框
下一题按钮
{answered && (
<TouchableOpacity style={s.nextBtn} onPress={handleNext}>
<Text style={s.nextBtnText}>
{currentIndex < questions.length - 1 ? '下一题' : '查看结果'}
</Text>
</TouchableOpacity>
)}
只有作答后才显示。最后一题显示"查看结果"。
结果页面
测试结束后显示成绩:
{finished && (
<View style={s.resultContainer}>
<Text style={s.resultIcon}>🎉</Text>
<Text style={s.resultTitle}>测试完成!</Text>
<Text style={s.resultScore}>{score} 分</Text>
<Text style={s.resultDesc}>
{score >= 80 ? '太厉害了,你是狗狗专家!' :
score >= 60 ? '不错哦,继续加油!' :
'还需要多了解狗狗哦~'}
</Text>
<TouchableOpacity style={s.retryBtn} onPress={handleRetry}>
<Text style={s.retryBtnText}>再测一次</Text>
</TouchableOpacity>
</View>
)}
根据分数显示不同的评语。
重新测试
const handleRetry = () => {
api.getBreeds().then(breeds => {
const qs = generateQuestions(breeds, 10);
setQuestions(qs);
setCurrentIndex(0);
setScore(0);
setAnswered(false);
setSelectedAnswer(null);
setFinished(false);
});
};
重新生成题目,重置所有状态。
随机排序的原理
sort(() => Math.random() - 0.5) 是怎么实现随机排序的?
Math.random() 返回 0 到 1 之间的随机数。减去 0.5 后,结果在 -0.5 到 0.5 之间。
sort 的比较函数返回负数时,第一个元素排前面;返回正数时,第二个元素排前面。
因为返回值随机正负,所以排序结果也是随机的。
这种方法简单但不是完美的随机。对于测试题目这种场景够用了。更严格的随机可以用 Fisher-Yates 洗牌算法。
防止重复题目
当前实现可能出现重复的错误选项。优化一下:
const wrongAnswers = breeds
.filter(b => b.id !== breed.id)
.sort(() => Math.random() - 0.5)
.slice(0, 3)
.map(b => b.name);
filter 已经排除了正确答案,所以不会重复。
但如果品种数据里有重名的(虽然不太可能),可以用 Set 去重:
const uniqueOptions = [...new Set([...wrongAnswers, breed.name])];
题目难度控制
可以根据品种的知名度调整难度:
简单模式:选项都是差异明显的品种,如金毛、哈士奇、柯基。
困难模式:选项是相似的品种,如拉布拉多和金毛、哈士奇和阿拉斯加。
实现思路是给品种打标签,生成选项时选择同标签的品种作为干扰项。
答题时间限制
可以加个倒计时增加紧张感:
const [timeLeft, setTimeLeft] = useState(15);
useEffect(() => {
if (answered || finished) return;
const timer = setInterval(() => {
setTimeLeft(t => {
if (t <= 1) {
handleAnswer(''); // 超时算错
return 15;
}
return t - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [currentIndex, answered, finished]);
每题 15 秒,超时自动跳过。
小结
品种测试功能涉及的知识点:
- 题目生成:从 API 数据动态生成题目和选项
- 随机排序:打乱数组顺序的简单方法
- 答题流程:选择、判断、计分、下一题的状态管理
- 条件样式:根据答题状态显示不同的选项样式
- 结果展示:根据分数显示不同评语
游戏化功能能让 App 更有趣,但要注意不能喧宾夺主。品种测试是锦上添花,核心还是品种信息的展示。
下一篇讲图库页面,是图片模块的入口,会涉及到多种图片浏览方式的整合。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)