從0開始,手把手教你開發並部署上線一個知識測驗微信小程式

[豆約翰]發表於2020-07-09

上線專案演示

微信搜尋[放馬來答]或掃以下二維碼體驗:

專案原始碼

專案原始碼

其他版本

Vue答題App實戰教程

Hello小程式

1.註冊微信小程式

點選立即註冊,選擇微信小程式,按照要求填寫資訊

2.登入小程式並完善資訊

填寫小程式資訊,完善資訊。

3.下載小程式開發工具

完善資訊後點選文件,工具,下載,選擇穩定版的對應平臺的安裝包下載,下載完後點選安裝即可


4.建立小程式專案

掃碼登入,選擇小程式,並點選加號,填寫相關資訊,APPID位置於下方截圖所示。

5.小程式程式碼結構介紹

如下圖所示的四個檔案,主要用於註冊和配置微信小程式,其包含的是全域性配置資訊。

  • app.js:用於註冊微信小程式應用。

  • app.json:小程式的全域性配置,比如網路請求的超時時間,以及視窗的屬性

  • app.wxss:小程式全域性樣式

  • project.config.json:包含了小程式的整體配置資訊,即使是換了開發裝置,亦或是換了專案,只要將該檔案保留,每個開發者的個性化設定就都將保留。

如下圖所示,還有兩個目錄,

  • pages:每一個子資料夾代表了小程式的一個頁面,比如index,和logs分別代表了兩個頁面。每個頁面又由四個檔案組成:
    index.js:處理頁面邏輯和資料互動。
    index.json:對應頁面的配置資訊。
    index.wxml:展示頁面的內容和元素。
    index.wxss:設定用wxml展示元素的樣式。

  • utils:存放的是一些工具程式碼,實現程式碼複用的目的。

6.小程式helloworld

開發試題分類頁面

新增home頁面

pages目錄下新建home目錄,並新增4個檔案,如圖所示:

其中:
home.js

// pages/home/home.js
Page({
  data: {

  },
  onLoad: function (options) {

  },
  toTestPage: function(e){
    let testId = e.currentTarget.dataset['testid'];
    wx.navigateTo({
      url: '../test/test?testId='+testId
    })
  }
})

home.wxml

<!--pages/home/home.wxml-->
<view class="page">
  <view class="page-title">請選擇試題:</view>
  <view class="flex-box">
    <view class="flex-item"><view class="item bc_green" bindtap="toTestPage" data-testId="18">IT知識</view></view>
    <view class="flex-item"><view class="item bc_red" bindtap="toTestPage" data-testId="15">遊戲知識</view></view>
    <view class="flex-item"><view class="item bc_yellow" bindtap="toTestPage" data-testId="21">體育知識</view></view>
    <view class="flex-item"><view class="item bc_blue" bindtap="toTestPage" data-testId="27">動物知識</view></view>
    <view class="flex-item item-last"><view class="item bc_green" bindtap="toTestPage" data-testId="0">綜合知識</view></view>
  </view>
</view>

home.json

{
  "navigationBarTitleText": "試題分類",
  "usingComponents": {}
}

home.wxss

/* pages/home/home.wxss */
.page-title {
  padding-top: 20rpx;
  padding-left: 40rpx;
  font-size: 16px;
}
.flex-box {
  display: flex;
  align-items:center;
  flex-wrap: wrap;
  justify-content: space-between;
  padding: 20rpx;
  box-sizing:border-box;
}
.flex-item {
  width: 50%;
  height: 200rpx;
  padding: 20rpx;
  box-sizing:border-box;
}
.item {
  width:100%;
  height:100%;
  border-radius:8rpx;
  display: flex;
  align-items:center;
  justify-content: center;
  color: #fff;
}
.item-last {
  flex: 1;
}

修改app.json,注意:pages/home/home一定要放到pages陣列的最前,以成為首頁。

{
  "pages": [
    "pages/home/home",
    "pages/index/index",
    "pages/logs/logs",
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "WeChat",
    "navigationBarTextStyle": "black"
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json"
}

修改app.wxss,定義全域性樣式

/**app.wxss**/
.container {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  padding: 200rpx 0;
  box-sizing: border-box;
} 

.bc_green{
    background-color: #09BB07;
}
.bc_red{
    background-color: #F76260;
}
.bc_blue{
    background-color: #10AEFF;
}
.bc_yellow{
    background-color: #FFBE00;
}
.bc_gray{
    background-color: #C9C9C9;
}

執行結果

開發試題展示功能

新增test頁面

pages目錄下新建test目錄,並新增4個檔案,如圖所示:

修改test.js

// pages/test/test.js
Page({

  /**
   * 頁面的初始資料
   */
  data: {
    test_id:0
  },

  /**
   * 生命週期函式--監聽頁面載入
   */
  onLoad: function (options) {
    this.setData({test_id:options.testId})
  }
	})

修改test.wxml

<!--pages/test/test.wxml-->
<text>我是{{test_id}}</text>

執行結果

在試題分類頁點選某一分類,跳轉到試題頁,試題頁顯示分類id

Mock試題資料

專案目錄下新增data目錄,data目錄新增json.js,存放我們的試題模擬資料。

var json = {
  "18": [
    {
      "question": "This mobile OS held the largest market share in 2012.?",
      "option": {
        "A": "iOS",
        "B": "Android",
        "C": "BlackBerry",
        "D": "Symbian"
      },
      "true": "A",
      "scores": 10,
      "checked": false
    },
    {
      "question": "This mobile OS held the largest market share in 2012.?",
      "option": {
        "A": "iOS",
        "B": "Android",
        "C": "BlackBerry",
        "D": "Symbian"
      },
      "true": "A",
      "scores": 10,
      "checked": false
    }
  ],
  "0": [
    {
      "question": "This mobile OS held the largest market share in 2012.?",
      "option": {
        "A": "iOS",
        "B": "Android",
        "C": "BlackBerry",
        "D": "Symbian"
      },
      "true": "A",
      "scores": 10,
      "checked": false
    },
    {
      "question": "This mobile OS held the largest market share in 2012.?",
      "option": {
        "A": "iOS",
        "B": "Android",
        "C": "BlackBerry",
        "D": "Symbian"
      },
      "true": "A",
      "scores": 10,
      "checked": false
    }
  ]
}

module.exports = {
  questionList: json
}

修改app.js

var jsonList = require('data/json.js');

App({
...
  },
  globalData: {
    questionList: jsonList.questionList,  // 拿到答題資料
    // questionList:{},
    quizCategory:{
      "0": "綜合知識",
      "18": "IT知識",
      "21": "體育知識",
      "15": "遊戲知識",
      "27":"動物知識",
    }
  }
})

修改test.js

// import he from "he";
var app = getApp();

Page({
  data: {
    bIsReady: false, // 頁面是否準備就緒
    index: 0,  // 題目序列
    chooseValue: [], // 選擇的答案序列
  },

  shuffle(array) {
    return array.sort(() => Math.random() - 0.5);
  },
  /**
   * 生成一個從 start 到 end 的連續陣列
   * @param start
   * @param end
   */
  generateArray: function (start, end) {
    return Array.from(new Array(end + 1).keys()).slice(start)
  },

  onLoad: function (options) {
    wx.setNavigationBarTitle({ title: options.testId }) // 動態設定導航條標題
    this.setData({
      questionList: app.globalData.questionList[options.testId],  // 拿到答題資料
      testId: options.testId // 課程ID
    })
    let countArr = this.generateArray(0, this.data.questionList.length - 1); // 生成題序
    this.setData({
      bIsReady: true,
      shuffleIndex: this.shuffle(countArr).slice(0, countArr.length) // 生成隨機題序 [2,0,3] 並擷取num道題
    })
  },
  /*
  * 單選事件
  */
  radioChange: function (e) {
    console.log('checkbox發生change事件,攜帶value值為:', e.detail.value)
    this.data.chooseValue[this.data.index] = e.detail.value;
    console.log(this.data.chooseValue);
  },
  /*
  * 退出答題 按鈕
  */
  outTest: function () {
    wx.showModal({
      title: '提示',
      content: '你真的要退出答題嗎?',
      success(res) {
        if (res.confirm) {
          console.log('使用者點選確定')
          wx.switchTab({
            url: '../home/home'
          })
        } else if (res.cancel) {
          console.log('使用者點選取消')
        }
      }
    })
  },
  /*
  * 下一題/提交 按鈕
  */
  nextSubmit: function () {
    // 如果沒有選擇
    if (this.data.chooseValue[this.data.index] == undefined) {
      wx.showToast({
        title: '請選擇至少一個答案!',
        icon: 'none',
        duration: 2000,
        success: function () {
          return;
        }
      })
      return;
    }

    // 判斷是不是最後一題
    if (this.data.index < this.data.shuffleIndex.length - 1) {
      // 渲染下一題
      this.setData({
        index: this.data.index + 1
      })
    } else {
      console.log('ok')
    }
  }
})

test.wxml

<!--pages/test/test.wxml-->
<view wx:if="{{bIsReady}}" class="page">
  <!--標題-->
  <view class='page__hd'>
    <view class="page__title">
      {{index+1}}、{{questionList[shuffleIndex[index]].question}}
      ({{questionList[shuffleIndex[index]].scores}}分)
    </view>
  </view>
  <!--內容-->
  <view class="page__bd">

    <radio-group class="radio-group" bindchange="radioChange">
      <label class="radio my-choosebox" wx:for="{{questionList[shuffleIndex[index]].option}}" wx:for-index="key"  wx:for-item="value">
        <radio value="{{key}}" checked="{{questionList[shuffleIndex[index]].checked}}"/>{{key}}、{{value}}
      </label>
    </radio-group>

  </view>
  <!--按鈕-->
  <view class='page_ft'>
    <view class='mybutton'>
      <button bindtap='nextSubmit' wx:if="{{index == questionList.length-1}}">提交</button>
      <button bindtap='nextSubmit' wx:else>下一題</button>
      <text bindtap='outTest' class="toindex-btn">退出答題</text>
    </view>
  </view>
</view>

test.wxss

/* pages/test/test.wxss */
.page {
  padding: 20rpx;
}
.page__bd {
  padding: 20rpx;
}
.my-choosebox {
  display: block;
  margin-bottom: 20rpx;
}
.toindex-btn {
  margin-top: 20rpx;
  display:inline-block;
  line-height:2.3;
  font-size:13px;
  padding:0 1.34em;
  color:#576b95;
  text-decoration:underline;
  float: right;
}

專案執行結果:

請求真實資料

修改test.js

  getQuestions(testId) {
    // 顯示標題欄載入效果
    wx.showNavigationBarLoading();
    wx.request({
      // url: 'https://opentdb.com/api.php?amount=10&difficulty=easy&type=multiple&category=' + testId,
      url: 'https://opentdb.com/api.php?amount=10&difficulty=easy&type=multiple&category=' + testId,
      method: "GET",
      success: res => {
        if (res.data.response_code === 0) {
          this.setData({
            questionList: this.parseQuestion(res.data.results),  // 拿到答題資料
            testId: testId // 課程ID
          })
          console.log(this.data.questionList);
          app.globalData.questionList[testId] = this.data.questionList
          let count = this.generateArray(0, this.data.questionList.length - 1); // 生成題序

          this.setData({
            bIsReady: true,
            shuffleIndex: this.shuffle(count).slice(0, 10) // 生成隨機題序 [2,0,3] 並擷取num道題
          })
        } else {
          ;
        }
        // 停止載入效果
        wx.stopPullDownRefresh();
        wx.hideNavigationBarLoading();
      },
      fail: err => {
        // 停止載入效果
        wx.stopPullDownRefresh();
        wx.hideNavigationBarLoading();
      }
    });

  },

  onLoad: function (options) {
    this.getQuestions(options.testId)
    console.log(options);

    wx.setNavigationBarTitle({ title: app.globalData.quizCategory[options.testId] }) // 動態設定導航條標題
  },

解析返回的資料:

   // 主題列表資料模型
  parseQuestion(aList) {
    let aTopicList = [];
    if (!aList || (aList && !Array.isArray(aList))) {
      aList = [];
    }

    aTopicList = aList.map(oItem => {

      const answers = [oItem.correct_answer, oItem.incorrect_answers].flat()
      let optionArr = ['A', 'B', 'C', 'D']
      let options = {}
      let optionArrShuffle = this.shuffle(optionArr)
      for (let i = 0; i < answers.length; i++) {
        options[optionArr[i]] = String(answers[i]);
      }
      const ordered_options = {};
      Object.keys(options).sort().forEach(function (key) {
        ordered_options[key] = options[key];
      });
      return {
        "question": String(oItem.question), // id
        "scores": 10,
        "checked": false,
        "option": ordered_options,
        "true": optionArr[0]
      };
    });
    return aTopicList;

  },

這裡解析的原因是,介面返回的json資料和我們自己設計的資料格式略有不同,我們要轉換成自己的資料格式:
介面返回的資料格式:

{
    "response_code": 0,
    "results": [{
        "category": "Science: Computers",
        "type": "multiple",
        "difficulty": "easy",
        "question": "The numbering system with a radix of 16 is more commonly referred to as ",
        "correct_answer": "Hexidecimal",
        "incorrect_answers": ["Binary", "Duodecimal", "Octal"]
    }, {
        "category": "Science: Computers",
        "type": "multiple",
        "difficulty": "easy",
        "question": "This mobile OS held the largest market share in 2012.",
        "correct_answer": "iOS",
        "incorrect_answers": ["Android", "BlackBerry", "Symbian"]
    }, {
        "category": "Science: Computers",
        "type": "multiple",
        "difficulty": "easy",
        "question": "How many values can a single byte represent?",
        "correct_answer": "256",
        "incorrect_answers": ["8", "1", "1024"]
    }, {
        "category": "Science: Computers",
        "type": "multiple",
        "difficulty": "easy",
        "question": "In computing, what does MIDI stand for?",
        "correct_answer": "Musical Instrument Digital Interface",
        "incorrect_answers": ["Musical Interface of Digital Instruments", "Modular Interface of Digital Instruments", "Musical Instrument Data Interface"]
    }, {
        "category": "Science: Computers",
        "type": "multiple",
        "difficulty": "easy",
        "question": "In computing, what does LAN stand for?",
        "correct_answer": "Local Area Network",
        "incorrect_answers": ["Long Antenna Node", "Light Access Node", "Land Address Navigation"]
    }]
}

我們自己的資料格式:

var json = {
  "18": [
    {
      "question": "This mobile OS held the largest market share in 2012.?",
      "option": {
        "A": "iOS",
        "B": "Android",
        "C": "BlackBerry",
        "D": "Symbian"
      },
      "true": "A",
      "scores": 10,
      "checked": false
    },
    {
      "question": "This mobile OS held the largest market share in 2012.?",
      "option": {
        "A": "iOS",
        "B": "Android",
        "C": "BlackBerry",
        "D": "Symbian"
      },
      "true": "A",
      "scores": 10,
      "checked": false
    }
  ],
  "0": [
    {
      "question": "This mobile OS held the largest market share in 2012.?",
      "option": {
        "A": "iOS",
        "B": "Android",
        "C": "BlackBerry",
        "D": "Symbian"
      },
      "true": "A",
      "scores": 10,
      "checked": false
    },
    {
      "question": "This mobile OS held the largest market share in 2012.?",
      "option": {
        "A": "iOS",
        "B": "Android",
        "C": "BlackBerry",
        "D": "Symbian"
      },
      "true": "A",
      "scores": 10,
      "checked": false
    }
  ]
}

注意:

開發期間:不校驗合法域名,web-view.....這裡不要勾選。

引入第三方庫

細心的朋友可能會發現,有些題目中有亂碼,如下圖所示&#039;

有一個很好的第三方庫He可以處理這個問題。

我們需要使用npm匯入一個第三方庫處理這個問題,大家會學習到在小程式開發中如何使用npm引入第三方庫。

專案根目錄下新建package.json檔案

{
  "name": "wechatanswer-master",
  "version": "1.0.0",
  "description": "答題類微信小程式",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://gitee.com/kamiba/my_quiz_wechat_app.git"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "he": "^1.2.0"
  }
}

2、執行npm install --production xxx,這個xxx就是你想使用的npm 包。此時在當前資料夾下會生成一個node_modules資料夾。PS:此處請務必使用--production選項,可以減少安裝一些業務無關的 npm 包,從而減少整個小程式包的大小。

npm install --production he

3、在微信開發者工具-->工具-->構建npm,此時會生成一個miniprogram_npm資料夾。

4、構建完成後就可以使用 npm 包了。首先把使用npm模組勾起來,然後在js檔案中引入即可。

然後修改test.js

import he from "he";

  // 主題列表資料模型
  parseQuestion(aList) {
    let aTopicList = [];
    if (!aList || (aList && !Array.isArray(aList))) {
      aList = [];
    }

    aTopicList = aList.map(oItem => {

      const answers = [oItem.correct_answer, oItem.incorrect_answers].flat()
      let optionArr = ['A', 'B', 'C', 'D']
      let options = {}
      let optionArrShuffle = this.shuffle(optionArr)
      for (let i = 0; i < answers.length; i++) {
        options[optionArr[i]] = he.decode(String(answers[i]));
      }
      const ordered_options = {};
      Object.keys(options).sort().forEach(function (key) {
        ordered_options[key] = options[key];
      });
      return {
        "question": he.decode(String(oItem.question)), // id
        "scores": 10,
        "checked": false,
        "option": ordered_options,
        "true": optionArr[0]
      };
    });
    return aTopicList;

  },

上面和以前有三處修改

import he from "he";
options[optionArr[i]] = he.decode(String(answers[i]));
"question": he.decode(String(oItem.question)), 

計算使用者得分,記錄錯題

修改test.js

Page({
  data: {
    bIsReady: false, // 頁面是否準備就緒
    index: 0,  // 題目序列
    chooseValue: [], // 選擇的答案序列
    totalScore: 100, // 總分
    wrong: 0, // 錯誤的題目數量
    wrongListSort: [], // 錯誤的題目集合
  },
 /*
  * 下一題/提交 按鈕
  */
  nextSubmit: function () {
    // 如果沒有選擇
    if (this.data.chooseValue[this.data.index] == undefined) {
      wx.showToast({
        title: '請選擇至少一個答案!',
        icon: 'none',
        duration: 2000,
        success: function () {
          return;
        }
      })
      return;
    }
    // 判斷答案是否正確
    this.chooseError();

    // 判斷是不是最後一題
    if (this.data.index < this.data.shuffleIndex.length - 1) {
      // 渲染下一題
      this.setData({
        index: this.data.index + 1
      })
    } else {
      let wrongListSort = JSON.stringify(this.data.wrongListSort);
      let chooseValue = JSON.stringify(this.data.chooseValue);
      let shuffleIndex = JSON.stringify(this.data.shuffleIndex);
      console.log('wrongListSort:' + wrongListSort)
      wx.navigateTo({
        url: '../results/results?totalScore=' + this.data.totalScore + '&shuffleIndex=' + shuffleIndex + '&chooseValue=' + chooseValue + '&wrongListSort=' + wrongListSort + '&testId=' + this.data.testId
      })
      // 設定快取


      var logs = wx.getStorageSync('logs') || []

      let logsList = { "date": Date.now(), "testId": app.globalData.quizCategory[this.data.testId], "score": this.data.totalScore }
      logs.unshift(logsList);
      wx.setStorageSync('logs', logs);
    }
  },
  /*
* 錯題處理
*/
  chooseError: function () {
    var trueValue = this.data.questionList[this.data.shuffleIndex[this.data.index]]['true'];
    var chooseVal = this.data.chooseValue[this.data.index];
    console.log('選擇了' + chooseVal + '答案是' + trueValue);
    if (chooseVal.toString() != trueValue.toString()) {
      console.log('錯了');
      this.data.wrong++;
      this.data.wrongListSort.push(this.data.index);
      this.setData({
        totalScore: this.data.totalScore - this.data.questionList[this.data.shuffleIndex[this.data.index]]['scores']  // 扣分操作
      })
    }
  },

實現結果展示頁面

image-20200709112925606

pages目錄下新增page---results

results.js

// pages/results/results.js
var app = getApp();
Page({
  data: {
    totalScore: null, // 分數
    shuffleIndex: [], // 錯誤的題數-亂序
    wrongListSort: [],  // 錯誤的題數-正序
    chooseValue: [], // 選擇的答案
    remark: ["好極了!你很棒棒哦", "哎喲不錯哦", "別灰心,繼續努力哦!"], // 評語
    modalShow: false
  },
  onLoad: function (options) {
    console.log(options);
    wx.setNavigationBarTitle({ title: app.globalData.quizCategory[options.testId] }) // 動態設定導航條標題

    let shuffleIndex = JSON.parse(options.shuffleIndex);
    let wrongListSort = JSON.parse(options.wrongListSort);
    let chooseValue = JSON.parse(options.chooseValue);
    this.setData({
      totalScore: options.totalScore != "" ? options.totalScore : "無",
      shuffleIndex: shuffleIndex,
      wrongListSort: wrongListSort,
      chooseValue: chooseValue,
      questionList: app.globalData.questionList[options.testId],  // 拿到答題資料
      testId: options.testId  // 課程ID
    })
    console.log(this.data.chooseValue);
  },
  // 檢視錯題
  toView: function () {
    // 顯示彈窗
    this.setData({
      modalShow: true
    })
  },
  // 返回首頁
  toIndex: function () {
    wx.switchTab({
      url: '../home/home'
    })
  }
})

results.json

{
  "navigationBarTitleText": "WeChatTest",
  "usingComponents": {
    "wrong-modal":"/components/wrongModal/wrongModal"
  }
}

results.wxml

<view class="page">
  <!--標題-->
  <view class='page-head'>
    <view class="page-title">
      答題結束!您的得分為:
    </view>
    <!--分數-->
    <view class='page-score'>
      <text class="score-num">{{totalScore}}</text>
      <text class="score-text">分</text>
    </view>
    <text class="score-remark">{{totalScore==100?remark[0]:(totalScore>=80?remark[1]:remark[2])}}</text>  <!-- 評價 -->
  </view>
  <!--查詢錯誤-->
  <view class='page-footer'>
    <view class="wrong-view" wx:if="{{wrongListSort.length > 0}}">
      <text>錯誤的題數:</text>
      <text wx:for="{{wrongListSort}}">[{{item-0+1}}]</text> 題
    </view>
    <view class="wrong-btns">
      <button type="default" bindtap="toView" hover-class="other-button-hover" class="wrong-btn" wx:if="{{wrongListSort.length > 0}}"> 點選檢視 </button>
      <button type="default" bindtap="toIndex" hover-class="other-button-hover" class="wrong-btn"> 返回首頁 </button>
    </view>
  </view>
  <wrong-modal modalShow="{{modalShow}}" shuffleIndex="{{shuffleIndex}}" wrongListSort="{{wrongListSort}}" chooseValue="{{chooseValue}}" questionList="{{questionList}}" testId="{{testId}}"
    ></wrong-modal>
</view>

results.wxss

/* pages/results/results.wxss */
.page {
  padding: 20rpx;
}
.page-head {
  text-align: center;
}
.page-title {

}
.page-score {
  display: flex;
  justify-content: center;
  align-items: flex-end;
  padding-top:40rpx;
  padding-bottom:40rpx;
}
.score-num {
  font-size:100rpx;
}
.page-footer {
  margin-top:60rpx;
  text-align: center;
}
.wrong-btns {
  display:flex;
  align-items:center;
  justify-content:center;
  margin-top: 60rpx;
}
.wrong-btn {
  margin-left:20rpx;
  margin-right:20rpx;
  height:70rpx;
  line-height:70rpx;
  font-size:14px;
}

實現錯題檢視頁面

image-20200709113002927

專案目錄下新增components目錄。components目錄下新增wrongModal目錄,wrongModal目錄下新增page---wrongModal

wrongModal.js

// components/wrongModal/wrongModal.js
Component({
  /**
   * 元件的屬性列表
   */
  properties: {
    // 是否顯示
    modalShow: {
      type: Boolean,
      value: false
    },
    // 題庫
    questionList: {
      type: Array,
      value: []
    },
    // 課程ID
    testId: {
      type: String,
      value: '101-1'
    },
    // 題庫亂序index
    shuffleIndex: {
      type: Array,
      value: []
    },

    // 錯題題數-正序
    wrongListSort: {
      type: Array,
      value: []
    },
    // 選擇的答案
    chooseValue: {
      type: Array,
      value: []
    }
  },
  /**
   * 元件的初始資料
   */
  data: {
    index: 0 // wrongList的index
  },
  /**
   * 元件所在頁面的生命週期
   */
  pageLifetimes: {
    show: function () {
      // 頁面被展示
      console.log('show')
      console.log(this.data.questionList)
      // console.log(this.data.wrongList)
    },
    hide: function () {
      // 頁面被隱藏
    },
    resize: function (size) {
      // 頁面尺寸變化
    }
  },
  /**
   * 元件的方法列表
   */
  methods: {
    // 下一題
    next: function(){
      if (this.data.index < this.data.wrongListSort.length - 1){
        // 渲染下一題
        this.setData({
          index: this.data.index + 1
        })
      }
    },
    // 關閉彈窗
    closeModal: function(){
      this.setData({
        modalShow: false
      })
    },
    // 再來一次
    again: function(){
      wx.reLaunch({
        url: '../test/test?testId=' + this.data.testId
      })
    },
    // 返回首頁
    toIndex: function () {
      wx.reLaunch({
        url: '../home/home'
      })
    },
  }
})

wrongModal.json

{
  "component": true,
  "usingComponents": {}
}

wrongModal.wxml

<!--components/wrongModal/wrongModal.wxml-->
<view class="modal-page" wx:if="{{modalShow}}">
  <view class="modal-mask" bindtap="closeModal"></view>
  <!-- 內容 -->
  <view class="modal-content">
    <view class="modal-title">
      題目: {{questionList[shuffleIndex[wrongListSort[index]]].question}} 
    </view>
    <view class="modal-body">
      <radio-group class="radio-group" bindchange="radioChange">
        <label class="radio my-choosebox" wx:for="{{questionList[shuffleIndex[wrongListSort[index]]].option}}" wx:for-index="key"  wx:for-item="value">
          <radio disabled="{{true}}" value="{{key}}" checked="{{questionList[shuffleIndex[wrongListSort[index]]].checked}}"/>{{key}}、{{value}}
        </label>
      </radio-group>
    </view>
    <!-- 答案解析 -->
    <view class="modal-answer">
      <text class="answer-text wrong-answer">
        您的答案為 {{chooseValue[wrongListSort[index]]}}
      </text>
      <text class="answer-text true-answer">
        正確答案為 {{questionList[shuffleIndex[wrongListSort[index]]]['true']}}
      </text>
    </view>
    <!-- 操作按鈕 -->
    <view class="modal-button">
      <view wx:if="{{index == wrongListSort.length-1}}" class="modal-btns">
        <button bindtap='again' class="modal-btn">再來一次</button>
        <button bindtap='toIndex' class="modal-btn">返回首頁</button>
      </view>
      <button bindtap='next' wx:else class="modal-btn">下一題</button>
    </view>
  </view>
</view>

wrongModal.wxss

/* components/wrongModal/wrongModal.wxss */
.modal-mask {
  position:fixed;
  width:100%;
  height:100%;
  top:0;
  left:0;
  z-index:10;
  background: #000;
  opacity: 0.5;
  overflow: hidden;
}
.modal-page {
  display:flex;
  align-items:center;
  justify-content:center;
  width:100%;
  height:100%;
  top:0;
  position:absolute;
  left:0;
}
.modal-content {
  width: 80%;
  min-height: 80%;
  background: #fff;
  border-radius: 8rpx;
  z-index:11;
  padding: 20rpx;
}
.modal-title {
  font-size: 14px;
}
.modal-body {
  padding: 20rpx;
}
.my-choosebox {
  display: block;
  margin-bottom: 20rpx;
  font-size: 14px;
}
.modal-answer {
  display: flex;
}
.answer-text {
  font-size: 14px;
  margin-right: 20rpx;
}
.modal-button {
  display: flex;
  align-items:center;
  justify-content:center;
  margin-top:60rpx;
}
.modal-btns {
  display: flex;
  align-items:center;
  justify-content:center;
}
.modal-btn {
  margin-left:20rpx;
  margin-right:20rpx;
  height:70rpx;
  line-height:70rpx;
  font-size:12px;
}

實現成績記錄頁面

image-20200709113214508

修改專案目錄下的app.json,增加底部導航欄

image-20200709113141222

{
  "pages": [
    "pages/home/home",
    "pages/logs/logs",
    "pages/test/test",
    "pages/results/results",
    "pages/index/index"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "WeChat",
    "navigationBarTextStyle": "black"
  },
  "tabBar": {
    "color": "#666666",
    "selectedColor": "#3cc51f",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [
      {
        "pagePath": "pages/home/home",
        "iconPath": "image/icon_component.png",
        "selectedIconPath": "image/icon_component_HL.png",
        "text": "答題"
      },
      {
        "pagePath": "pages/logs/logs",
        "iconPath": "image/icon_API.png",
        "selectedIconPath": "image/icon_API_HL.png",
        "text": "記錄"
      }
    ]
  },
  "sitemapLocation": "sitemap.json"
}

專案目錄下新增image資料夾,並新增以下檔案,具體檔案請下載原始碼獲取:

image-20200709113945088

修改pages目錄下logs頁面

logs.js

//logs.js
const util = require('../../utils/util.js')

Page({
  data: {
    logs: [],
  },
  onShow: function() {
    this.setData({
      logs: this.formatLogs()
    })
  },
  // 拿到快取並格式化日期資料
  formatLogs: function(){
    let newList = [];
    (wx.getStorageSync('logs') || []).forEach(log => {
      if(log.date){
        log['date'] = util.formatTime(new Date(log.date));
        newList.push(log);
      }
    })
    return newList;
  }
})

logs.json

{
  "navigationBarTitleText": "檢視日誌",
  "usingComponents": {}
}

logs.wxml

<!--logs.wxml-->
<view class="page">
  <view class="table" wx:if="{{logs.length>0}}">
    <view class="tr bg-w">
      <view class="th first">時間</view>
      <view class="th">試題</view>
      <view class="th ">得分</view>
    </view>
    <block wx:for="{{logs}}" wx:for-item="item">
      <view class="tr">
        <view class="td first">{{item.date}}</view>
        <view class="td">{{item.testId}}</view>
        <view class="td">{{item.score}}</view>
      </view>
    </block>
  </view>
  <view class="no-record" wx:else>
    <image src="/image/wechat.png" class="no-image"></image>
    <text class="no-text">沒有資料哦~</text>
  </view>
</view>

logs.wxss

.table {
 border: 0px solid darkgray;
 font-size: 12px;
}
.tr {
 display: flex;
 width: 100%;
 justify-content: center;
 height: 2rem;
 align-items: center;
}
.td {
  width:40%;
  justify-content: center;
  text-align: center;
}
.bg-w{
 background: snow;
}
.th {
 width: 40%;
 justify-content: center;
 background: #3366FF;
 color: #fff;
 display: flex;
 height: 2rem;
 align-items: center;
}
.first {
  flex:1 0 auto;
}
.no-record {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.no-image {
  width: 200rpx;
  height: 200rpx;
  margin-top: 200rpx;
  margin-bottom: 40rpx;
}
.no-text {
  font-size: 16px;
  color: #ccc;
  display: block;
}

修改test.js

  /*
  * 下一題/提交 按鈕
  */
  nextSubmit: function () {
    // 如果沒有選擇
	.....
      // 設定快取

      var logs = wx.getStorageSync('logs') || []

      let logsList = { "date": Date.now(), "testId": app.globalData.quizCategory[this.data.testId], "score": this.data.totalScore }
      logs.unshift(logsList);
      wx.setStorageSync('logs', logs);
    }
  },

最終專案結構

image-20200709113800865

小程式部署上線

租用伺服器,申請域名、搭建Linux環境並配置https服務

釋出體驗版

這裡的體驗版釋出階段有點類似於我們日常的灰度釋出前的內部灰度測試,可以指定白名單使用者進行生產測試體驗。釋出的動作其實很簡單,就是在微信的開發IDE中上面工具欄上點選上傳按鈕即可釋出到微信伺服器,提交後就可以在mp管理端檢視到新的開發版本,可以釋出二維碼白名單使用者掃碼後進行體驗。

上線稽核

體驗版本驗證沒問題後就可以釋出,點選開發版本右邊的提交稽核按鈕就可以釋出到騰訊進行小程式稽核,第一次釋出稽核時間會比較長,大約3-5個工作日左右,日後的升級版本稽核就很快了,基本上可以做到半天就稽核通過。

上線

稽核通過後就會出現在稽核版本的欄位,點選右邊的釋出即可,釋出後監控一段後臺服務情況,如果發現問題可以緊急回退版本,小程式mp管理端也提供了前端版本回退的功能。
總體來說小程式在版本管理、釋出回退的體驗方面做得還是很好的,能夠滿足基本需求,不需要額外的開發。

專案總結

很感謝您和豆約翰走到了這裡,至此我們這個知識測驗微信小程式,全部開發完畢。如果對於程式碼或小程式上線有任何疑問,可以加我微信[tiantiancode]一起討論。

最後

如果您覺得豆約翰的文章對您有所幫助,另外不想錯過豆約翰未來更多更好的技術實戰教程,還請

相關文章