# 设计分析

## 问题数据的定义

`````` export const questions = [];

## 解析数据的定义

``````export const parseObject = {
"en":{
output:"",//输出文本
successMsg:"",//用户回答正确文本
errorMsg:"",//用户回答错误文本
detail:[],//问题答案解析文本
tabs:[],//中英文切换选项数组
title:"",//首页标题文本
startContent:"",//首页段落文本
endContent:"",//解析页段落文本
startBtn:"",//首页开始按钮文本
endBtn："",//解析页重新开始文本
},
"zh":{
//选项同en属性值一致
}
}

``````export function marked(template) {
let result = "";
result = template.replace(/\[.+?\]\(.+?\)/g,word => {
const link = word.slice(word.indexOf('(') + 1, word.indexOf(')'));
const linkText = word.slice(word.indexOf('[') + 1, word.indexOf(']'));
}).replace(/\*\*\*([\s\S]*?)\*\*\*[\s]?/g,text => '<code>' + text.slice(3,text.length - 4) + '</code>');
return result;
}

``````[xxx](xxx)

``````/\[.+?\]\(.+?\)/g;

``````***//code***

``````/\*\*\*([\s\S]*?)\*\*\*[\s]?/g

## 其它文本的定义

``````export function getCurrentQuestion(lang="en",order= 1,total = questions.length){
return lang === 'en' ? `Question \${ order } of \${ total }` : `第\${ order }题，共\${ total }题`;
}
export function getCurrentAnswers(lang = "en",correctNum = 0,total= questions.length){
return lang === 'en' ? `You got \${ correctNum } out of \${ total } correct!` : `共 \${ total }道题，您答对了 \${ correctNum } 道题！`;
}

# 实现思路分析

## 中英文选项卡切换组件

``````<div class="tab-container">
<div class="tab-item">en</div>
<div class="tab-item">zh</div>
</div>

``````import React from "react";
import { parseObject } from '../data/data';
import "../style/lang.css";
export default class LangComponent extends React.Component {
constructor(props){
super(props);
this.state = {
activeIndex:0
};
}
onTabHandler(e){
const { nativeEvent } = e;
const { classList } = nativeEvent.target;
if(classList.contains('tab-item') && !classList.contains('tab-active')){
const { activeIndex } = this.state;
let newActiveIndex = activeIndex === 0 ? 1 : 0;
this.setState({
activeIndex:newActiveIndex
});
this.props.changeLang(newActiveIndex);
}
}
render(){
const { lang } = this.props;
const { activeIndex } = this.state;
return (
<div className="tab-container" onClick = { this.onTabHandler.bind(this) }>
{
parseObject[lang]["tabs"].map(
(tab,index) =>
(
<div className={`tab-item \${ activeIndex === index ? 'tab-active' : ''}`}  key={tab}>{ tab }</div>
)
)
}
</div>
)
}
}

css样式代码如下:

``````.tab-container {
display: flex;
align-items: center;
justify-content: center;
border:1px solid #f2f3f4;
position: fixed;
top: 15px;
right: 15px;
}
.tab-container > .tab-item {
color: #e7eaec;
cursor: pointer;
transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.tab-container > .tab-item:first-child {
}
.tab-container > .tab-item:last-child {
}
.tab-container > .tab-item.tab-active,.tab-container > .tab-item:hover {
color: #fff;
}

`js`逻辑，我们可以看到我们通过父组件传递一个`lang`参数用来确定中英文模式，然后开始访问定义数据上的`tabs`,即数组，`react.js`渲染列表通常都是使用`map`方法。事件代理，我们可以看到我们是通过获取原生事件对象`nativeEvent`拿到类名，判断元素是否含有`tab-item`类名，从而确定点击的是子元素，然后调用`this.setState`更改当前的索引项，用来确定当前是哪项被选中。由于只有两项，所以我们可以确定当前索引项不是`0`就是`1`，并且我们也暴露了一个事件`changeLang`给父元素以便父元素可以实时的知道语言模式的值。

## 底部内容组件

``````import React from "react";
import "../style/bottom.css";
const BottomComponent = (props) => {
return (
<div className="bottom" id="bottom">{ props.children }</div>
)
}
export default BottomComponent;

CSS代码如下:

``````.bottom {
position: fixed;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 18px;
}

## 内容组件的实现

``````import React from "react";
import "../style/content.css";
const ContentComponent = (props) => {
return (
<p className="content">{ props.children }</p>
)
}
export default ContentComponent;

CSS样式代码如下:

``````.content {
max-width: 35rem;
width: 100%;
line-height: 1.8;
text-align: center;
font-size: 18px;
color: #fff;
}

## 渲染HTML字符串的组件

``````import "../style/render.css";
export function createMarkup(template) {
return { __html: template };
}
const RenderHTMLComponent = (props) => {
const { template } = props;
let renderTemplate = typeof template === 'string' ? template : "";
return <div dangerouslySetInnerHTML={createMarkup( renderTemplate )} className="render-content"></div>;
}
export default RenderHTMLComponent;

CSS样式代码如下:

``````.render-content a,.render-content{
color: #fff;
}
.render-content a {
border-bottom:1px solid #fff;
text-decoration: none;
}
.render-content code {
color: #245cd4;
background-color: #e5e2e2;
font-size: 16px;
display: block;
white-space: pre;
margin: 15px 0;
word-break: break-all;
overflow: auto;
}
.render-content a:hover {
color:#efa823;
border-color: #efa823;
}

## 标题组件的实现

``````import React from "react";
const TitleComponent = (props) => {
let TagName = `h\${ props.level || 1 }`;
return (
<React.Fragment>
<TagName>{ props.children }</TagName>
</React.Fragment>
)
}
export default TitleComponent;

``````import React, { FunctionComponent,ReactNode }from "react";
interface propType {
level:number,
children?:ReactNode
}
//这一行代码是需要的
type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
const TitleComponent:FunctionComponent<propType> = (props:propType) => {
//这里断言一下只能是h1~h6的标签名
let TagName = `h\${ props.level }` as HeadingTag;
return (
<React.Fragment>
<TagName>{ props.children }</TagName>
</React.Fragment>
)
}
export default TitleComponent;

## 按钮组件的实现

``````import React from "react";
import "../style/button.css";
export default class ButtonComponent extends React.Component {
constructor(props){
super(props);
this.state = {
typeArr:["primary","default","danger","success","info"],
sizeArr:["mini",'default',"medium","normal","small"]
}
}
onClickHandler(){
this.props.onClick && this.props.onClick();
}
render(){
const { nativeType,type,long,size,className,forwardedRef } = this.props;
const { typeArr,sizeArr } = this.state;
const buttonType = type && typeArr.indexOf(type) > -1 ? type : 'default';
const buttonSize = size && sizeArr.indexOf(size) > -1 ? size : 'default';
let longClassName = '';
let parentClassName = '';
if(className){
parentClassName = className;
}
if(long){
longClassName = "long-btn";
}
return (
<button
ref={forwardedRef}
type={nativeType}
className={ `btn btn-\${ buttonType } \${ longClassName } btn-size-\${buttonSize} \${parentClassName}`}
onClick={ this.onClickHandler.bind(this)}
>{ this.props.children }</button>
)
}
}

CSS样式代码如下:

``````.btn {
outline: none;
display: inline-block;
border: 1px solid var(--btn-default-border-color);
color: var(--btn-default-font-color);
background-color: var(--btn-default-color);
font-size: 14px;
letter-spacing: 2px;
cursor: pointer;
}
.btn.btn-size-default {
}
.btn.btn-size-mini {
}
.btn:not(.btn-no-hover):hover,.btn:not(.btn-no-active):active,.btn.btn-active {
border-color: var(--btn-default-hover-border-color);
background-color: var(--btn-default-hover-color);
color:var(--btn-default-hover-font-color);
}
.btn.long-btn {
width: 100%;
}

``````:root {
--btn-default-color:transparent;
--btn-default-border-color:#d8dbdd;
--btn-default-font-color:#ffffff;
--btn-default-hover-color:#fff;
--btn-default-hover-border-color:#a19f9f;
--btn-default-hover-font-color:#535455;
/* 1 */
/* 2 */
/* 3 */
/* 4 */
}

## 问题选项组件

``````import React from "react";
import { QuestionArray } from "../data/data";
import ButtonComponent from './buttonComponent';
import TitleComponent from './titleComponent';
import "../style/quiz-wrapper.css";
export default class QuizWrapperComponent extends React.Component {
constructor(props:PropType){
super(props);
this.state = {

}
}
onSelectHandler(select){
this.props.onSelect && this.props.onSelect(select);
}
render(){
const { question } = this.props;
return (
<div className="quiz-wrapper flex-center flex-direction-column">
<TitleComponent level={1}>{ question.question }</TitleComponent>
<div className="button-wrapper flex-center flex-direction-column">
{
<ButtonComponent
nativeType="button"
onClick={ this.onSelectHandler.bind(this,select)}
className="mt-10 btn-no-hover btn-no-active"
key={select}
long
>{ select }</ButtonComponent>
))
}
</div>
</div>
)
}
}

css样式代码如下:

``````.quiz-wrapper {
width: 100%;
height: 100vh;
max-width: 600px;
}
.App {
height: 100vh;
overflow:hidden;
}
.App h1 {
color: #fff;
font-size: 32px;
letter-spacing: 2px;
margin-bottom: 15px;
text-align: center;
}
.App .button-wrapper {
max-width: 25rem;
width: 100%;
display: flex;
}
* {
margin: 0;
box-sizing: border-box;
}
body {
height:100vh;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
animation:background 50s linear infinite;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.mt-10 {
margin-top: 10px;
}
.ml-5 {
margin-left: 5px;
}
.text-align {
text-align: center;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-direction-column {
flex-direction: column;
}
.w-100p {
width: 100%;
}
::-webkit-scrollbar {
width: 5px;
height: 10px;
}
::-webkit-scrollbar-thumb {
width: 5px;
height: 5px;
}
@keyframes background {
0% {
}
25%,50% {
}
50%,75% {
}
100% {
}
}

## 解析组件

``````import React from "react";
import { parseObject,questions } from "../data/data";
import { marked } from "../utils/marked";
import RenderHTMLComponent from './renderHTML';
import "../style/parse.css";
export default class ParseComponent extends React.Component {
constructor(props){
super(props);
this.state = {};
}
render(){
const { lang,userAnswers } = this.props;
const setTypeClassName = (index) =>
return (
<ul className="result-list">
{
parseObject[lang].detail.map((content,index) => (
<li
className={`result-item \${ setTypeClassName(index) }`} key={content}>
<span className="result-question">
<span className="order">{(index + 1)}.</span>
{ questions[index].question }
</span>
<div className="result-item-wrapper">
{ parseObject[lang].output }:<span className="ml-5 result-correct-answer-value">{ questions[index].correct }</span>
</span>
</span>
<span
{
? parseObject[lang].successMsg
: parseObject[lang].errorMsg
}
</span>
<RenderHTMLComponent template={ marked(content) }></RenderHTMLComponent>
</div>
</li>
))
}
</ul>
)
}
}

CSS样式代码如下:

``````.result-wrapper {
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
.result-wrapper .result-list {
list-style: none;
width: 100%;
max-width: 600px;
}
.result-wrapper .result-list .result-item {
background-color: #020304;
margin-bottom: 2rem;
color: #fff;
}
.result-content .render-content {
max-width: 600px;
line-height: 1.5;
font-size: 18px;
}
.result-wrapper .result-question {
background-color: #1b132b;
font-size: 22px;
letter-spacing: 2px;
}
.result-wrapper .result-question .order {
margin-right: 8px;
}
.result-wrapper .result-item-wrapper,.result-wrapper .result-list .result-item {
display: flex;
flex-direction: column;
}
.result-wrapper .result-item-wrapper {
}
letter-spacing: 1px;
}
font-weight: bold;
font-size: 20px;
}
max-width: 250px;
margin:1rem 0;
}
background-color: #d82323;
}
background-color: #4ee24e;
}

``````const setTypeClassName = (index) => `answered-\${ questions[index].correct === userAnswers[index] ? 'correctly' : 'incorrectly'}`;

### 1.题目信息

``````<span className="result-question">
<span className="order">{(index + 1)}.</span>
{ questions[index].question }
</span>

### 2.正确答案

`````` <span className="result-correct-answer">
{ parseObject[lang].output }:
</span>

### 3.用户回答

``````<span className="result-user-answer">
</span>

### 4.提示信息

``````<span className={`inline-answer \${ setTypeClassName(index) }`}>
{
? parseObject[lang].successMsg
: parseObject[lang].errorMsg
}
</span>

### 5.答案解析

``````<RenderHTMLComponent template={ marked(content) }></RenderHTMLComponent>

## 回到顶部按钮组件

``````import React, { useEffect } from "react";
import ButtonComponent from "./buttonComponent";
import "../style/top.css";
const TopButtonComponent = React.forwardRef((props, ref) => {
const svgRef = React.createRef();
const setPathElementFill = (paths, color) => {
if (paths) {
Array.from(paths).forEach((path) => path.setAttribute("fill", color));
}
};
const onMouseEnterHandler = () => {
const svgPaths = svgRef?.current?.children;
if (svgPaths) {
setPathElementFill(svgPaths, "#2396ef");
}
};
const onMouseLeaveHandler = () => {
const svgPaths = svgRef?.current?.children;
if (svgPaths) {
setPathElementFill(svgPaths, "#ffffff");
}
};
const onTopHandler = () => {
props.onClick && props.onClick();
};
return (
<ButtonComponent
onClick={onTopHandler.bind(this)}
className="to-Top-btn btn-no-hover btn-no-active"
size="mini"
forwardedRef={ref}
>
{props.children ? ( props.children) : (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4158"
onMouseEnter={onMouseEnterHandler.bind(this)}
onMouseLeave={onMouseLeaveHandler.bind(this)}
ref={svgRef}
>
<path
d="M508.214279 842.84615l34.71157 0c0 0 134.952598-188.651614 134.952598-390.030088 0-201.376427-102.047164-339.759147-118.283963-357.387643-12.227486-13.254885-51.380204-33.038464-51.380204-33.038464s-37.809117 14.878872-51.379181 33.038464C443.247638 113.586988 338.550111 251.439636 338.550111 452.816063c0 201.378473 134.952598 390.030088 134.952598 390.030088L508.214279 842.84615zM457.26591 164.188456l50.948369 0 50.949392 0c9.344832 0 16.916275 7.522324 16.916275 16.966417 0 9.377578-7.688099 16.966417-16.916275 16.966417l-50.949392 0-50.948369 0c-9.344832 0-16.917298-7.556093-16.917298-16.966417C440.347588 171.776272 448.036711 164.188456 457.26591 164.188456zM440.347588 333.852624c0-37.47859 30.387078-67.865667 67.865667-67.865667s67.865667 30.387078 67.865667 67.865667-30.387078 67.865667-67.865667 67.865667S440.347588 371.331213 440.347588 333.852624z"
p-id="4159"
fill={props.color}
></path>
<path
d="M460.214055 859.812567c-1.87265 5.300726-2.90005 11.000542-2.90005 16.966417 0 12.623505 4.606925 24.189935 12.244882 33.103956l21.903869 37.510312c1.325182 8.052396 8.317433 14.216793 16.750499 14.216793 8.135284 0 14.929014-5.732561 16.585747-13.386892l0.398066 0 24.62177-42.117237c5.848195-8.284687 9.29469-18.425651 9.29469-29.325909 0-5.965875-1.027399-11.665691-2.90005-16.966417L460.214055 859.81359z"
p-id="4160"
fill={props.color}
></path>
<path
d="M312.354496 646.604674c-18.358113 3.809769-28.697599 21.439288-23.246447 39.399335l54.610782 179.871647c3.114944 10.304693 10.918677 19.086707 20.529569 24.454972l8.036024-99.843986c1.193175-14.745842 11.432377-29.226648 24.737404-36.517705-16.502859-31.912827-34.381042-71.079872-49.375547-114.721835L312.354496 646.604674z"
p-id="4161"
fill={props.color}
></path>
<path
d="M711.644481 646.604674l-35.290761-7.356548c-14.994506 43.641963-32.889061 82.810031-49.374524 114.721835 13.304004 7.291057 23.544229 21.770839 24.737404 36.517705l8.036024 99.843986c9.609869-5.368264 17.397229-14.150278 20.529569-24.454972L734.890928 686.004009C740.34208 668.043962 730.003618 650.414443 711.644481 646.604674z"
p-id="4162"
fill={props.color}
></path>
</svg>
)}
</ButtonComponent>
);
}
);
const TopComponent = (props) => {
const btnRef = React.createRef();
let scrollElement= null;
let top_value = 0,timer = null;
const updateTop = () => {
top_value -= 20;
scrollElement && (scrollElement.scrollTop = top_value);
if (top_value < 0) {
if (timer) clearTimeout(timer);
scrollElement && (scrollElement.scrollTop = 0);
btnRef.current && (btnRef.current.style.display = "none");
} else {
timer = setTimeout(updateTop, 1);
}
};
const topHandler = () => {
scrollElement = props.scrollElement?.current || document.body;
top_value = scrollElement.scrollTop;
updateTop();
props.onClick && props.onClick();
};
useEffect(() => {
const scrollElement = props.scrollElement?.current || document.body;
// listening the scroll event
scrollElement && scrollElement.addEventListener("scroll", (e: Event) => {
const { scrollTop } = e.target;
if (btnRef.current) {
btnRef.current.style.display = scrollTop > 50 ? "block" : "none";
}
});
});
return (<TopButtonComponent ref={btnRef} {...props} onClick={topHandler.bind(this)}></TopButtonComponent>);
};
export default TopComponent;

CSS样式代码如下:

``````.to-Top-btn {
position: fixed;
bottom: 15px;
right: 15px;
display: none;
transition: all .4s ease-in-out;
}
.to-Top-btn .icon {
width: 35px;
height: 35px;
}

``````const setPathElementFill = (paths, color) => {
//将颜色值和path标签数组作为参数传入，然后设置fill属性值
if (paths) {
Array.from(paths).forEach((path) => path.setAttribute("fill", color));
}
};

``````const updateTop = () => {
top_value -= 20;
scrollElement && (scrollElement.scrollTop = top_value);
if (top_value < 0) {
if (timer) clearTimeout(timer);
scrollElement && (scrollElement.scrollTop = 0);
btnRef.current && (btnRef.current.style.display = "none");
} else {
timer = setTimeout(updateTop, 1);
}
};

## app组件的实现

``````import React, { useReducer, useState } from "react";
import "../style/App.css";
import LangComponent from "../components/langComponent";
import TitleComponent from "../components/titleComponent";
import ContentComponent from "../components/contentComponent";
import ButtonComponent from "../components/buttonComponent";
import BottomComponent from "../components/bottomComponent";
import QuizWrapperComponent from "../components/quizWrapper";
import ParseComponent from "../components/parseComponent";
import RenderHTMLComponent from '../components/renderHTML';
import TopComponent from '../components/topComponent';
import { getCurrentQuestion, parseObject,questions,getCurrentAnswers,QuestionArray } from "../data/data";
import { LangContext, lang } from "../store/lang";
import { OrderReducer, initOrder } from "../store/count";
import { marked } from "../utils/marked";
import { computeSameAnswer } from "../utils/same";
let collectionCorrectAnswers [] = questions.reduce((v,r) => {
v.push(r.correct);
return v;
},[]);
let correctNum = 0;
function App() {
const [langValue, setLangValue] = useState(lang);
const [correctTotal,setCorrectTotal] = useState(0);
const [orderState,orderDispatch] = useReducer(OrderReducer,0,initOrder);
const changeLangHandler = (index: number) => {
const value = index === 0 ? "en" : "zh";
setLangValue(value);
};
const startQuestionHandler = () => orderDispatch({ type:"reset",payload:1 });
const endQuestionHandler = () => {
correctNum = 0;
};
const onSelectHandler = (select:string) => {
// console.log(select)
orderDispatch({ type:"increment"});
if(orderState.count > 25){
}
if(select){
}
setCorrectTotal(correctNum);
}
const { count:order } = orderState;
const wrapperRef = React.createRef();
return (
<div className="App flex-center">
<LangContext.Provider value={langValue}>
<LangComponent lang={langValue} changeLang={changeLangHandler}></LangComponent>
{
order > 0 ? order <= 25 ?
(
<div className="flex-center flex-direction-column w-100p">
<QuizWrapperComponent
question={ questions[(order - 1 < 0 ? 0 : order - 1)] }
onSelect={ onSelectHandler }
>
</QuizWrapperComponent>
<BottomComponent lang={langValue}>{getCurrentQuestion(langValue, order)}</BottomComponent>
</div>
)
:
(
<div className="w-100p result-wrapper" ref={wrapperRef}>
<div className="flex-center flex-direction-column result-content">
<RenderHTMLComponent template={marked(parseObject[langValue].endContent)}></RenderHTMLComponent>
<div className="button-wrapper mt-10">
<ButtonComponent nativeType="button" long onClick={endQuestionHandler}>
{parseObject[langValue].endBtn}
</ButtonComponent>
</div>
</div>
<TopComponent scrollElement={wrapperRef} color="#ffffff"></TopComponent>
</div>
)
:
(
<div className="flex-center flex-direction-column">
<TitleComponent level={1}>{parseObject[langValue].title}</TitleComponent>
<ContentComponent>{parseObject[langValue].startContent}</ContentComponent>
<div className="button-wrapper mt-10">
<ButtonComponent nativeType="button" long onClick={startQuestionHandler}>
{parseObject[langValue].startBtn}
</ButtonComponent>
</div>
</div>
)
}
</LangContext.Provider>
</div>
);
}
export default App;

``````export function computeSameAnswer(correct = 0,userAnswer,correctAnswers,index) {
correct++;
}
return correct;
}

``````import { createContext } from "react";
export let lang = "en";
export const LangContext = createContext(lang);

``````export function initOrder(initialCount) {
return { count: initialCount };
}
export function OrderReducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
default:
throw new Error();
}
}

``````const startQuestionHandler = () => orderDispatch({ type:"reset",payload:1 });
const endQuestionHandler = () => {
correctNum = 0;
};
const onSelectHandler = (select:string) => {
// console.log(select)
orderDispatch({ type:"increment"});
if(orderState.count > 25){
}
if(select){
}
setCorrectTotal(correctNum);
}