因为今天已经来到倒数第二天!
觉得应该要把之前介绍过的东西全部融合在一起展现出来,
也算是成果展吧XD
今天要用 Next.js 搭配 Bootstrap + React + Reactstrap,
做出问答小游戏!
然後今天原谅我可能没办法像前几天那样讲得很细Q_Q
之前我们只有把 Next.js 的环境布好,
但今天还会用到 Bootstrap 跟 Reactstrap,
因此我们也需要在专案目录下进行安装,
先移到 nextjs-blog
资料夹底下,
执行以下指令:
npm install bootstrap@next
npm install reactstrap
再来就是在 pages
资料夹下新增页面 game.jsx
,
在 game.jsx
最前面记得 import,
像这样:
import React, { useState, useEffect } from "react";
import { Container, Row, Col, Button } from "reactstrap"; //这边放你会用到的 component
import 'bootstrap/dist/css/bootstrap.css';
虽然最後的 code 看起来已经用 React 的方式写好了,
不过我个人习惯都会是先写死,
先把架构弄出来,
之後再把它改成 React 语法就不会是太大的问题,
像这样:
<Container className="w-50 my-4">
<div className="d-flex flex-column align-items-center">
<h3>本届东京奥运台湾的表现相当亮眼,缔造队史最佳参赛成绩。最终夺牌数以下何者正确?</h3>
<img width="300rem" src={`img/01.jpg`}/>
<Row md={2} className="mt-4">
<Col className="my-2">
<Button size="sm" color="info" className="w-50 px-2 py-3">
1金、2银、3铜
</Button>
</Col>
<Col className="my-2">
<Button size="sm" color="info" className="w-50 px-2 py-3">
2金、4银、6铜
</Button>
</Col>
<Col className="my-2">
<Button size="sm" color="info" className="w-50 px-2 py-3">
2金、4银、8铜
</Button>
</Col>
<Col className="my-2">
<Button size="sm" color="info" className="w-50 px-2 py-3">
3金、3银、6铜
</Button>
</Col>
</Row>
</div>
</Container>
再来因为我想要分成三个画面:开始游戏前、游戏中、游戏结束,
所以我宣告了一个 status 的状态,分别为:initial, start, end
(PS. 游戏中才是游戏的核心,所以之前的写死资料我只试游戏中的部份而已)
... (略)
const [status, setStatus] = useState('initial');
{status === 'initial' && (
... (这边放开始游戏前的欢迎画面)
)}
{status === 'start' && (
... (这边放开始游戏的画面)
)}
{status === 'end' && (
... (这边放游戏结束的画面)
)}
再来我新增了data
的资料夹,
里面放 question.json
,
内容就是题库跟选项,
像这样:
[
{
"question": "本届东京奥运台湾的表现相当亮眼,缔造队史最佳参赛成绩。最终夺牌数以下何者正确?",
"img": "01.jpg",
"options": ["1金、2银、3铜","2金、4银、6铜","2金、4银、8铜","3金、3银、6铜"],
"answer": 2
},
{
"question": "以下运动项目与本届奥运台湾出赛选手的配对,何者「有误」?",
"img": "02.png",
"options": ["柔道─杨勇纬","举重─郭婞淳","射箭─汤智钧","桌球─戴资颖"],
"answer": 4
},
... (略)
]
这样的架构大家应该不陌生,里面就是物件的阵列,
每个物件分别有 question, img, options, answer 这四个属性,
分别为题目、图片、选项、正确答案。
而在前面 import 的地方记得将 json 档引入,
import questions from '../data/question.json';
既然前面都学过 map 了,我们当然不会想要 hard code 把每一题写死在页面上,
这边我先贴这部份的程序,然後再进行说明:
{status === 'start' ? (
<>
{questions.map((q, index_q) => (
<>
{index_q === selectedIndex ? (
<div className="d-flex flex-column align-items-center">
<h3>Q{index_q+1}. {q.question}</h3>
<img className="my-4" width="300rem" src={`img/${q.img}`}/>
<Row md={2}>
{q.options.map((o, index_o) => (
<>
<Col className="my-2">
<Button size="sm" color="info" value={index_o+1} className={`${styles.btn} px-2 py-3`}
onClick={(e) => {
handleResponse(e);
setSelectedIndex((prev) => prev+1);
}}>
{index_o+1}) {o}
</Button>
</Col>
</>
))}
</Row>
</div>
): null}
</>
))}
</>
): null}
{questions.map((q, index_q) => (
questions 就是先前引入的 json 里面的物件阵列,
而这边希望是一题一题出现,
因此我在前面有宣告一个索引,用来记录现在进行到哪一题:
const [selectedIndex, setSelectedIndex] = useState(0);
所以下面才会有 {index_q === selectedIndex ? (
,
例如现在进行到第1题,只要显示第1题的内容。(索引值为0)
<h3>Q{index_q+1}. {q.question}</h3>
再来这边是显示题目内容,
这边应该还算直觉,
再下来又出现一个 map 了!
原因是因为选项(options)也是一个阵列(每一题有4个选项),
题目都可以用 map 读出来了,
选项当然也可以呀~
因此这边才会这样写:
{q.options.map((o, index_o) => (
<>
... (略)
</>
))}
以下是选项的部份:
<Col className="my-2">
<Button size="sm" color="info" value={index_o+1} className={`${styles.btn} px-2 py-3`}
onClick={(e) => {
handleResponse(e);
setSelectedIndex((prev) => prev+1);
}}>
{index_o+1}) {o}
</Button>
</Col>
首先看到 onClick
里面有两行,
setSelectedIndex((prev) => prev+1)
这行就是将目前进行的题目索引值+1,
再来是 handleResponse(e)
这个函数主要就是进行分数的计算,
这个函数我是这麽写的:
const handleResponse = (event) => {
if ( parseInt(event.target.value) === questions[selectedIndex].answer ){
alert("恭喜答对!");
setTotalpoints((prev) => prev+1);
} else {
alert("可惜!请再接再励!");
}
}
当 event.target.value
(点击目标的 value) 等於 questions[selectedIndex].answer
(现在进行题目的正确答案),
分数就要+1 setTotalpoints((prev) => prev+1)
。
这边应该是这个游戏最关键的地方,
当你前面写完,发现每一题都很顺利的进行,
可是结束最後一题後画面变成空白了,
这是因为 questions 里面的阵列已经全部结束了,
它没有内容可以显示只能显示空白给你看啊XD
但是游戏结束并不是点击哪个按钮之类就是结束游戏,
因此不适合使用 useState,
所以这边我们要用到 useEffect 了!
useEffect(() => {
if ( selectedIndex === questions.length ){
setStatus('end');
}
},[selectedIndex]);
侦测题目索引值的变化,
一旦 selectedIndex 等於 questions.length(题库阵列长度) 表示游戏结束了,
因此要把状态切换成 end setStatus('end')
,
一旦状态为 end,
就要显示游戏结束以及显示总得分,
像这样:
{status === 'end' && (
<div className="d-flex flex-column align-items-center">
<h2>游戏结束!</h2>
<img className="my-2" width="500rem" src="img/end.jpg" />
<h3>
共获得 <span className="h1 text-danger">{totalpoints}</span> 分
</h3>
<Button color="secondary" onClick={resetGame}>
重新游戏
</Button>
</div>
)}
前面的地方都有用到 <img>
因此你可能有发现 src 的设定不是网址,
而是相对位置的档案位址 src="img/end.jpg"
,
介绍一下 Next.js 的档案架构:
图片供外部存取的档案都会放在 public 资料夹底下,
因此你可以直接使用 档名.副档名
就可以指到该档案的位址,
至於前面多了 img/
是因为我把图片放在 img 的资料夹底下,
像这样:
我们之前在 Codesandbox import CSS 是这样写的:
import "./styles.css";
可是当你在 Next.js 引入 CSS 这样写却会出错:
import styles from '../styles/styles.css';
原因是 XXX.css
在 Next.js 被视为是 Global CSS,
详细可看官方文件说明→ Global CSS Must Be in Your Custom <App>
如果你真的要自己写 CSS,
只能将档案命名为 XXX.module.css
了,
像这样:
import styles from '../styles/styles.module.css';
而使用 CSS 的语法是这样:
... className={`${styles.btn} px-2 py-3`} ...
如果 className 只有要放一个单独自订的 class,语法这样写就好:className={styles.btn}
上面这个写法是因为还有跟其他 Bootstrap 的 class 摆在一起才要这样写。(ES6 的写法)
[10/4-补充] 因为铁人赛结束之後我应该不太会再打开这个服务了,
所以这边还是补一下 CodeSandbox 的连结,
如果想要自己玩玩看的,
不想先被我破梗的可以右转至此→ Day29 - React 奥运小学堂
------ 我是贴心的防雷设置 ------
------ 我是贴心的防雷设置 ------
------ 我是贴心的防雷设置 ------
然後下面是我试玩的画面XD
(不过因为题目答案都是我自己用的,所以当然是全部答对XD)
这个系列文可以用这个小游戏当成结尾真是太好了!!!!!!!
中间一度以为自己写不出来,
算是及格过关了XD
接着就开心迎接明天的完结篇吧XD
<<: 找LeetCode上简单的题目来撑过30天啦(DAY16)
>>: Day 16: LeetCode 698. Partition to K Equal Sum Subsets
第一页、第六页、第七页 序。 序放在第一页後面也是完全OKです! 因为第一天讲太多废话了,完全没有讲...
大家好,我是用python程序 在输出json档之後想做两件事但没有头绪,希望有人可以帮我解惑。 以...
今天我们要实作一支程序------计算平行四边形面积的程序。 题目如下:假设三角形的 底为a,高为b...
GoogLeNet Google提出的GoogLeNet,层数比较多,运算的效率相当好,超参数数量比...
这章节是延伸v-if和v-show管理元件,如何用更简便的方式做tab页签。 v-bind:is 做...