案例开源地址: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

Logo

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

更多推荐