从0开始,手把手教你开发并部署上线一个知识测验微信小程序

上线项目演示

微信搜索[放马来答]或扫以下二维码体验:

项目源码

项目源码

视频教程

视频教程

其他版本

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]一起讨论。

最后

如果您觉得豆约翰的文章对您有所帮助,另外不想错过豆约翰未来更多更好的技术实战教程,还请

posted @ 2020-07-09 15:31  [豆约翰]  阅读(3327)  评论(0编辑  收藏  举报