HUMAN INSIGHT CX
リサーチソリューション
コンサルティング
クリエイティブ・UX
制作・オペレーション
テクノロジー
データドリブン
マーケティング
サービスについて
オピニオン
トライベック広報
会社概要
社長メッセージ
経営理念
社名・ロゴの由来
トライベック・ニューノーマル
役員紹介
地図・アクセス

COLUMN

コラム

Nuxt.jsで選択式クイズアプリを作ってみた(2)

2020年11月9日
プロダクション

本コラムについて

本記事は前回のコラムの続きとなっております。
前回ご紹介したアプリの実際のソースと、工夫した点を記載しております。
(共通ファイルやcssについての説明は省いております。本記事の最後に全体ソースを公開しておりますので、そちらからご参照下さい。)

前回のコラム:Nuxt.jsで選択式クイズアプリを作ってみた

工夫した点

  • 問題文と選択肢は問題解答画面と解答結果画面の両方で呼び出しているため、コンポーネント化を行った。
  • 解答をやり直したい場合もあるのを考慮し、前の問題に【戻る】ボタンを設置した。
  • 解答結果画面では、自分が選択した解答と正しい答えが分かるようにした。
  • 繰り返し問題が解けるように、問題と選択肢のランダム機能を作成した。。

作成したファイル

  • ①メインページ(/pages/index.vue)
  • ②トップ画面コンポーネント(/components/top.vue)
  • ③問題解答画面コンポーネント(/components/questions.vue)
  • ④解答結果画面コンポーネント(/components/result.vue)
  • ⑤問題コンポーネント(/components/questions/qInfo.vue)
  • ⑥コンポーネント共通Storeファイル(/store/common.js)

ソースの中身

メインページ(①/pages/index.vue)

本アプリのページはこの1ページのみ。
画面中央に表示しているコンポーネント要素を切り替えることで、 複数の画面を表示させている。

<template>
  <div class="main">
    <div class="top">
      <top v-if="dispType === 'top'"></top>
      <questions v-if="dispType === 'questions'"></questions>
      <result v-if="dispType === 'result'"></result>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
import Top from '~/components/top.vue';
import Questions from '~/components/questions.vue';
import Result from '~/components/result.vue';
import settingsJsonPath from '~/assets/json/settings.json'; // 設定ファイル

export default {
  data() {
    return {
      
    }
  },
  computed: {
    ...mapState('common', [
      'dispType',
      'settingOption',
    ]),
  },
  created () {
    this.readSettings(); // 初期表示設定
  },
  components: {
    Top,
    Questions,
    Result
  },
  methods: {
    ...mapMutations('common', [
      'setSettingOption',
      'setDispType',
    ]),
    /**
     * 初期表示設定
     * 
     * 設定ファイルをstoreに格納し、トップコンポーネントを表示
     */
    readSettings() {
      this.setSettingOption(settingsJsonPath); // 設定オプション情報をstoreに格納
      this.setDispType('top'); // トップコンポーネントを表示
    },
  }
}
</script>

②トップ画面コンポーネント(/components/top.vue)

問題を選択する画面
汎用性を持たせるために、タイトルとボタンの表記は設定ファイルの内容から取得している。

<template>
  <div class="topComponent">
    <h2 class="title">{{title}}</h2>
    <div class="contents">
      <div class="select_question">
        <p>問題を選択</p>
        <ul>
          <li v-for="(fileInfo, index) in settingOption.questionPaths" :key="index" @click="selectQuestion(fileInfo.filePath)" :class="{'selected': selectQuestionPath === fileInfo.filePath}">
            {{fileInfo.title}}
          </li>
        </ul>
      </div>
      <div class="btns-area">
        <button class="btn" @click="showQuestionCoponent()" :disabled="!selectQuestionPath">{{btnName}}</button>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState, mapMutations } from "vuex";
import axios from 'axios';

export default {
  data() {
    return {
      title: '', // メインタイトル
      btnName: '', // ボタン名
      selectQuestionPath: '', // 選択した問題集のファイルパス
    }
  },
  created() {
    this.selectQuestionPath = ''; // 選択ファイルパスのリセット
    this.setSettingName(); // タイトルとボタン名を設定
  },
  computed: {
    ...mapState('common', [
      'questionList',
      'settingOption',
    ]),
  },
  methods: {
    ...mapMutations('common', [
      'setDispType',
      'setQuestionList',
    ]),
    /**
     * タイトルとボタン名を設定
     * 
     * 設定ファイルの内容からタイトルとボタン名を設定
     */
    setSettingName() {
      this.title = this.settingOption.title;
      this.btnName = this.settingOption.btnName;
    },
    /**
     * 問題コンポーネントを表示
     */
    showQuestionCoponent() {
      this.readQuestions(); // 問題情報を読み込む
    },
    /**
     * 問題情報を読み込む
     */
    readQuestions() {
      axios.get(this.selectQuestionPath).then((response) => {
        this.setReadQuestions(response.data.questionList);
        this.setDispType("questions");
      }).catch((error) => {
        console.log(error);
      })
    },
    /**
     * 読み込んだ問題情報をstoreに格納
     *
     * @param {object[]} questionList 問題情報
     */
    setReadQuestions(questionList) {
      // 問題をランダムにする
      if (this.settingOption.randam) {
        questionList = this.shuffleArray(questionList);
      }

      // 選択肢をランダムにする
      if (this.settingOption.choiceRandam) {
        questionList.forEach((questionInfo, index) => {
          questionInfo.choice = this.shuffleArray(questionInfo.choice);
        });
      }

      this.setQuestionList(questionList); // 問題情報をstoreに格納
    },
    /**
     * 選択した問題集のファイルパスを設定
     *
     * @param {string} filePath 問題集のファイルパス
     */
    selectQuestion(filePath) {
      // 既に選択している場合は選択状態を解除
      if (filePath === this.selectQuestionPath) {
        this.selectQuestionPath = '';
      } else {
        this.selectQuestionPath = filePath;
      }
    }
  },
};
</script>

③問題解答画面コンポーネント(/components/questions.vue)

問題を解答する画面
前の問題に【戻る】ボタン押下で1つ前に解答した問題に戻ることも出来る。
問題内容と選択肢は問題コンポーネントを呼び出して表示

<template>
  <div class="questionsComponent">
    <div class="header">
      <button class="btn btn-back" @click="showPreviousQuestion()" :disabled="qIndex === 0">前の問題に戻る</button>
    </div>
    <div class="contents">
      <div class="quetion-area">
        <question-info :qIndex="qIndex" :questionInfo="questionInfo" :answerList="selectAnswerList" :mode="'question'"></question-info>
      </div>
      <div class="btns-area">
        <button class="btn" @click="answerQuestion(qIndex+1)">解答</button>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations } from "vuex";
import QuestionInfo from '~/components/questions/qInfo.vue';

export default {
  data() {
    return {
      questionInfo: {}, // 表示する問題情報
      qIndex: 0, // 問題のインデックス
      correctCount: 0, // 正解数
      selectAnswerList: [], // 選択した解答リスト
    }
  },
  created() {
    this.showQustion(); // 表示する問題情報をセット
  },
  computed: {
    ...mapState('common', [
      'questionList',
    ]),
    ...mapGetters('common', [
      'getSelectChoiceStore',
    ]),
  },
  components: {
    QuestionInfo,
  },
  methods: {
    ...mapMutations('common', [
      'setDispType',
      'setCorrectCount',
      'setSelectAnswerList',
    ]),
    /**
     * 表示する問題情報をセット
     */
    showQustion() {
      this.questionInfo = this.questionList[this.qIndex];
    },
    /**
     * 解答ボタン押下処理
     * 
     * 答え合わせをして、次の問題を表示。最後の問題の場合は解答結果画面を表示。
     * 
     * @param {int} nextIndex 次の問題のインデックス
     */
    answerQuestion(nextIndex) {
      this.checkCorrectAnswer();

      this.qIndex = nextIndex;

      if (this.questionList[this.qIndex]) {
        this.selectChoise = '';
        this.showQustion();
      } else {
        this.setCorrectCount(this.correctCount);
        this.setSelectAnswerList(this.selectAnswerList);
        this.setDispType("result");
      }
    },
    /**
     * 前の問題に戻るボタン押下処理
     * 
     * 前の問題を表示。選択肢のラジオボタンも自分が解答した選択肢の値にする。
     */
    showPreviousQuestion() {
      this.qIndex = this.qIndex - 1; // 問題のインデックスを一つ前に設定
      this.showQustion(); // 前の問題を表示
      this.selectChoise = this.selectAnswerList[this.qIndex]; // 前の問題で選択した選択肢を設定
    },
    /**
     * 選択した問題の答えチェック
     * 
     * 正解の場合は正解数をインクリメントする。解答リストに自分が選択した解答を追加。
     */
    checkCorrectAnswer() {
      if (this.getSelectChoiceStore === this.questionInfo.answer) {
        this.correctCount++;
      }

      this.selectAnswerList[this.qIndex] = this.getSelectChoiceStore;
    }
  },
};
</script>

④解答結果画面コンポーネント(/components/result.vue)

答え合わせをする画面
正答率だけではなく、自分が選択した解答と正しい解答を色分けで表示している。

<template>
  <div class="resultComponent">
    <h2 class="title">全{{questionList.length}}問中、{{correctCount}}問正解です。</h2>
    <div class="contents">
      <div class="answers">
        <ul>
          <li v-for="(questionInfo, qIndex) in questionList" :key="qIndex">
            <question-info :qIndex="qIndex" :questionInfo="questionInfo" :mode="'answer'"></question-info>
          </li>
        </ul>
      </div>
      <div class="btns-area">
        <button class="btn" @click="backTop()">終了</button>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState, mapMutations } from "vuex";
import QuestionInfo from '~/components/questions/qInfo.vue';

export default {
  data() {
    return {

    }
  },
  created () {
    
  },
  computed: {
    ...mapState('common', [
      'correctCount',
      'questionList',
    ]),
  },
  components: {
    QuestionInfo,
  },
  methods: {
    ...mapMutations('common',[
            'setDispType',
    ]),
    /**
     * トップ画面に戻る
     */
    backTop() {
      this.setDispType("top");
    }
  },
};
</script>

⑤問題コンポーネント(/components/questions/qInfo.vue)

問題と選択肢を表示するコンポーネント
問題解答画面と解答結果画面の両方にて本コンポーネントを呼び出している。

<template>
    <div class="qInfoComponent">
        <div class="header">
            <p class="title">
                問{{qIndex+1}}
                <span v-if="mode === 'answer' && questionInfo.answer === selectAnswerList[qIndex]" class="answer-correct">: 正解</span>
                <span v-if="mode === 'answer' && questionInfo.answer !== selectAnswerList[qIndex]" class="answer-incorrect">: 不正解</span>
            </p>
            <p class="question" v-html="questionInfo.question"></p>
        </div>
        <div class="contents">
            <ul class="choiceList">
                <li v-for="(choice, index) in questionInfo.choice" :key="index" class="choiceList__item">
                    <label class="choice">
                        <input type="radio" v-model="selectChoice" :value="choice" :disabled="mode === 'answer' && (choice !== questionInfo.answer)">
                        <span :class="checkCorrectClass(choice)" v-html="choice"></span>
                    </label>
                </li>
            </ul>
        </div>
    </div>
</template>

<script>
import { mapState, mapMutations } from "vuex";

export default {
  data() {
    return {
            selectChoice: '', // 選択肢
    }
  },
  created () {
        this.initSelectChoice(); // 選択肢の初期値設定
    },
    watch: {
        questionInfo() {
            this.$nextTick(() => {
                this.initSelectChoice(); // 選択肢の初期値設定
            });
        },
        selectChoice(newVal) {
            this.setSelectChoiceStore(newVal); // 前の画面に戻るボタンの処理用に選択するたびに値を保持
        },
    },
  computed: {
    ...mapState('common', [
            'selectAnswerList',
            'settingOption',
    ]),
    },
    props: ['qIndex', 'questionInfo', 'mode', 'answerList'],
  methods: {
    ...mapMutations('common',[
            'setSelectChoiceStore',
        ]),
        /**
         * 選択肢の初期値設定
         * 
         * 問題解答画面の場合、初めて表示する問題の場合は先頭の選択肢にチェックが付けるが、一度解答した問題であれば自分が選択した解答にチェックが付く。
         */
        initSelectChoice() {
            if (this.mode === 'question') {
                this.selectChoice = this.answerList[this.qIndex] || this.questionInfo.choice[0];
            } else if (this.mode === 'answer') {
                this.selectChoice = this.questionInfo.answer;
            }
        },
        /**
         * 選択肢のクラス付与処理
         * 
         * 正解の選択肢に緑、自分が選択した不正解の選択肢なら赤の背景のクラスを付与
         * 
         * @param {string} choice 選択した選択肢
         * @return {string} 正解(is-correct)、もしくは不正解(is-incorrect)のクラスを返す
         */
        checkCorrectClass(choice) {
            if (this.mode !== 'answer') {
                return '';
            }

            if (choice === this.selectAnswerList[this.qIndex]) {
                if (choice === this.questionInfo.answer) {
                    return 'is-correct';
                } else {
                    return 'is-incorrect';
                }
            } else if (choice === this.questionInfo.answer) {
                return 'is-correct';
            }
        }
  },
};
</script>

⑥コンポーネント共通Storeファイル(/store/common.js)

コンポーネント間で共有するデータを管理させる。

const getDefaultState = () => {
  return {
    dispType: '', // 表示する画面情報
    settingOption: {}, // 設定ファイルの内容
    questionList: [], // 問題集リスト
    selectAnswerList: [], // 解答リスト
    correctCount: 0, // 正解数
    selectChoiceStore: '', // 選択した選択肢
    }
}

export const state = () => (getDefaultState())

export const getters = {
  getSelectChoiceStore (state) {
    return state.selectChoiceStore;
  },
}

export const mutations = {
  setDispType (state, value) {
    state.dispType = value;
  },
  setSettingOption (state, value) {
    state.settingOption = value;
  },
  setQuestionList (state, value) {
    state.questionList = value;
  },
  setSelectAnswerList (state, value) {
    state.selectAnswerList = value;
  },
  setCorrectCount (state, value) {
    state.correctCount = value;
  },
  setSelectChoiceStore (state, value) {
    state.selectChoiceStore = value;
  },
};

export const actions = {

}

本アプリの全体ソースについて

下記から入手できます。
https://github.com/ymtps/questionWebApp

この記事を書いた人

テクノロジーソリューション事業部

お問い合わせ

Web戦略策定からサイト構築、オペレーションまで、最適なワンストップのソリューションを提供します。
お気軽にお問い合わせください。

関連コンテンツ