这是之前受到朋友圈的一些 亲密度测试 的启发,开发的一个互动答题利用。

性能

  1. 创立自定义题目,设置正确选项
  2. 生成分享图邀请好友来答题
  3. 好友失去成绩单,并能够生成图片分享
  4. 告诉核心能够查看好友答题记录

技术栈

  • 前端:小程序
  • 后端:云开发
  • 框架:mpx

展现

小程序搜寻:小题卡。因为应用收费到云套餐,接口有拜访次数限度

预览地址

截图

代码

git仓库:https://github.com/luosijie/m...

代码构造

示例

因为残缺我的项目波及简略交互较多,上面展现一些次要性能代码

新建题目

外围要点:用 swipe组件 实现题目到卡片式切换
<template>  <view>    <swiper class="cards" bindchange="swiperChange" current="{{ current }}">      <!-- 全局配置 -->      <swiper-item>        <view class="card config">          <textarea type="text" maxlength="12" auto-height="true" wx:model="{{ formData.title }}"  placeholder="请输出题卡名称"/>        </view>      </swiper-item>      <swiper-item wx:for="{{ formData.cards }}" wx:key="id" wx:for-item="card">        <view class="card">          <!-- 题目 -->          <input type="text" maxlength="12" class="title" value="{{ card.title }}" placeholder="请输出题目" bindblur="titleChange"/>          <!-- 选项 -->          <view            class="option"            wx:for="{{ card.options }}"            wx:key="id"            wx:for-item="option"            wx:for-index="optionIndex"          >            <!-- 移除选项 -->            <i class="remove iconfont icon-remove" bindtap="removeOption(optionIndex)"></i>            <!-- 选项题目 -->            <input type="text" maxlength="12" value="{{ option.value }}" placeholder="请输出选项题目" bindblur="optionTitleChange(optionIndex, $event)"/>            <!-- 正确选项 -->            <view class="correct" bindtap="setCorrect(index, optionIndex)">              <i class="check iconfont icon-check" wx:if="{{ card.correct === optionIndex }}"></i>            </view>          </view>          <!-- 新增 -->          <view class="add-option" wx:if="{{ card.options.length < 4 }}" bindtap="addOption">新增选项</view>        </view>      </swiper-item>      <swiper-item>        <view class="card" bindtap="addCard">          <view class="new-card">            <i class="iconfont icon-new-card"></i>          </view>        </view>      </swiper-item>    </swiper>    <view class="controls">      <view class="ps">        <block wx:if="{{ current < formData.cards.length + 1 && current > 0 }}">          <view class="tip">            点击选项左边标记正确答案          </view>          <i class="delete-card iconfont icon-clear" bindtap="deleteCard" wx:if="{{ current > 1 }}"></i>        </block>      </view>      <view class="swip">        <view class="pre iconfont icon-pre" bindtap="pre"></view>        <view class="current">          <block wx:if="{{ current === 0 }}">            题卡配置          </block>          <block wx:elif="{{ current < formData.cards.length + 1 }}">            第{{ current }}/{{ formData.cards.length }}题          </block>          <block wx:else>            新增题目          </block>        </view>        <view class="next iconfont icon-next" bindtap="next"></view>      </view>      <view class="generate" bindtap="generate">生成</view>    </view>  </view></template><script>  import { createPage } from '@mpxjs/core'  createPage({    data: {      current: 0,      formData: {        title: '',        cards: []      }    },    onLoad () {      this.formData.cards = []      const card = this.generateCard()      this.formData.cards.push(card)    },    methods: {      // 生成选项      generateOption (id) {        return {          id,          value: ''        }      },      // 生成一个起始题目      generateCard () {        const id = new Date().getTime()        const card = {          id,          title: '',          options: [            this.generateOption(id + 1),            this.generateOption(id + 2),            this.generateOption(id + 3),            this.generateOption(id + 4)          ],          correct: 0        }        return card      },      addCard () {        const card = this.generateCard()        this.formData.cards.push(card)      },      swiperChange (e) {        this.current = e.detail.current      },      // 切换上一题      pre () {        if (this.current > 0) {          this.current--        }      },      // 切换下一题目      next () {        if (this.current < this.formData.cards.length + 1) {          this.current++          console.log('cards:', this.formData)        }      },      // 移除选项      removeOption (index) {        const options = this.formData.cards[this.current - 1].options        if (options.length < 3) {          wx.showToast({            title: '至多保留2个选项',            icon: 'none'          })          return        }        options.splice(index, 1)      },      // 新增选项      addOption () {        const options = this.formData.cards[this.current - 1].options        const option = this.generateOption(new Date().getTime())        options.push(option)      },      // 删除卡片      deleteCard () {        console.log('delete')        if (this.formData.cards.length < 2) {          wx.showToast({            title: '不能再删了',            icon: 'none'          })          return        }        wx.showModal({          title: '提醒',          content: '确定删除该题目吗',          confirmColor: '#40A9FF',          success: () => {            this.formData.cards.splice(this.current - 1, 1)            console.log('删除卡片', this.formData.cards)          }        })      },      titleChange (e) {        this.formData.cards[this.current - 1].title = e.detail.value        console.log('e', this.formData.cards)      },      optionTitleChange (index, e) {        const options = this.formData.cards[this.current - 1].options        options[index].value = e.detail.value      },      // 校验题卡表单      validateForm () {        if (!this.formData.title) {          wx.showToast({            title: '题卡名称未填写',            icon: 'none'          })          return false        }        for (let i = 0; i < this.formData.cards.length; i++) {          const card = this.formData.cards[i]          if (!card.title) {            wx.showToast({              title: `第${i + 1}道题 题目未填写`,              icon: 'none'            })            return false          }          // 校验选项题目填写状况          for (let j = 0; j < card.options.length; j++) {            const option = card.options[j]            if (!option.value) {              wx.showToast({                title: `第${i + 1}道题 选项未欠缺`,                icon: 'none'              })              return false            }          }        }        return true      },      // 设置正确选项      setCorrect (index, optionIndex) {        const card = this.formData.cards[index]        card.correct = optionIndex        this.$set(this.formData.cards, index, card)      },      async generate () {        const valid = this.validateForm()        if (!valid) return        wx.showLoading({          title: '解决中...',          mask: true        })        const res = await wx.cloud.callFunction({          name: 'questionAdd',          data: this.formData        })        if (res.result.success) {          wx.showToast({            title: '创立胜利',            icon: 'success'          })          // 跳转到投票详情页          setTimeout(() => {            wx.redirectTo({              url: `detail-entry?_id=${res.result._id}`            })          }, 1500)        }      }    }  })</script>// 省略款式

成绩单

外围要点:小程序canvas制作页面分享图
<template>  <view class="main" wx:if="{{ detail }}">    <view class="card">      <view class="title">“ {{ detail.question.title }} ”</view>      <view class="zql">正确率</view>      <view class="score">{{ detail.score }}</view>      <view class="from">        <image src="{{ detail.creator.avatarUrl }}"></image> {{ detail.creator.nickName }} 的成绩单      </view>      <view class="result" wx:if="{{ showResult }}">        <view          class="item"          wx:for="{{ detail.result }}"          wx:key="index"          wx:style="{{ { background: item.right ? '#4faf70' : '#d94948' } }}"        >          {{ item.letter }}        </view>      </view>      <view class="me-too" wx:else bindtap="toCreate">        我也来出一题      </view>      <view class="info">        <view class="date">          {{ detail.createTime }}        </view>        <view class="date">          出题人: {{ detail.questionCreator.nickName }}        </view>        <view class="num" wx:if="{{ detail.question.answers }}">          {{ detail.question.answers.length }}次参加        </view>      </view>    </view>    <view class="action">      <view class="share" bindtap="generateShareImage">生成分享图</view>      <view class="detail" bindtap="toCards" wx:if="{{ user.OPENID === detail.creator.OPENID }}">再试一次</view>      <view class="detail" bindtap="toDetail" wx:else>我试一下</view>    </view>    <!-- 用来生成分享图 -->    <canvas      type="2d"      id="canvas_share"      class="canvas-share"      style="width: {{canvasShare.width}}px; height: {{canvasShare.height}}px"    />    <pop visible="{{ imageShare.visible }}" bindclose="closeImageShare">      <image src="{{ imageShare.image }}" mode="aspectFit" class="image-share"></image>    </pop>  </view></template><script>  import { createPage } from '@mpxjs/core'  import no2letter from '../utils/no2letter'  import loadImage from '../utils/loadImage'  createPage({    data: {      user: null,      detail: null,      canvasShare: {        width: 0,        height: 0      },      imageShare: {        visible: false,        image: ''      },      showResult: false    },    onLoad (params) {      const id = params._id || params.scene      this.user = wx.getStorageSync('user')      this.getDetail(id)    },    onShareAppMessage () {      const title = `我在${this.detail.questionCreator.nickName}的题目中得分${this.detail.score},你也来试试?`      return {        title      }    },    methods: {      closeImageShare () {        this.imageShare.visible = false      },      // 生成分享图      async generateShareImage () {        if (this.imageShare.image) {          this.imageShare.visible = true          wx.saveImageToPhotosAlbum({            filePath: this.imageShare.image,            success () {              wx.showToast({                title: '图片曾经保留到相册',                icon: 'none'              })            },            fail () {              wx.showToast({                title: '请先在设置里关上相册权限',                icon: 'none'              })            }          })          return        }        wx.showLoading({          title: '解决中...'        })        const res = await wx.cloud.callFunction({          name: 'wxacode',          data: {            page: 'pages/result',            scene: this.detail._id          }        })        let pageCode        if (res.result.errCode === 0) {          pageCode = `data:image/png;base64,${wx.arrayBufferToBase64(res.result.buffer)}`        } else {          return        }        const query = this.createSelectorQuery()        query          .select('#canvas_share')          .fields({ node: true, size: true })          .exec(async res => {            console.log('ressss', res)            // 获取 canvas 实例            const canvas = res[0].node            // 获取 canvas 绘图上下文            const ctx = canvas.getContext('2d')            const width = 700            const height = 900            this.canvasShare.width = 700            this.canvasShare.height = 900            canvas.width = width            canvas.height = height            // 绘制背景            ctx.fillStyle = 'white'            ctx.fillRect(0, 0, width, height)            // 绘制head区域            ctx.textBaseline = 'top'            ctx.font = '32px sans-serif'            ctx.fillStyle = '#000000'            ctx.fillText('小题卡', 20, 20)            ctx.fillStyle = '#999999'            ctx.fillText('成绩单', 585, 20)            // 绘制title            const title = `“${this.detail.question.title}”`            ctx.font = 'normal bold 50px sans-serif'            ctx.fillStyle = '#000000'            ctx.fillText(title, (width - title.length * 50) / 2 + 25, 150)            // 绘制sub-title            const subtitle = '我在      的题目中正确率为'            ctx.font = '24px sans-serif'            ctx.fillStyle = '#999'            ctx.fillText(subtitle, (width - subtitle.length * 24) / 2 + 48, 250)            // 绘制出题者头像            const photoCreator = await loadImage.call(this, this.detail.questionCreator.avatarUrl, 'canvas_share')            ctx.drawImage(photoCreator, 263, 250, 24, 24)            // 绘制score            ctx.font = 'normal bold 200px sans-serif'            ctx.fillStyle = '#70B7FC'            ctx.fillText(this.detail.score, (width - this.detail.score.length * 100) / 2 - 80, 350)            // 绘制welcome            const welcome = '你也来试试吧'            ctx.font = '24px sans-serif'            ctx.fillStyle = '#999'            ctx.fillText(welcome, (width - welcome.length * 24) / 2, 620)            // 绘制创建人头像            const photoAnswer = await loadImage.call(this, this.detail.creator.avatarUrl, 'canvas_share')            ctx.drawImage(photoAnswer, 40, 780, 24, 24)            // 绘制foot-title            const footTitle = '邀请你一起来答题'            ctx.font = '24px sans-serif'            ctx.fillStyle = '#999'            ctx.fillText(footTitle, 70, 780)            // 绘制foot-title            const footSubTitle = '长按图片辨认进入小程序'            ctx.font = '24px sans-serif'            ctx.fillStyle = '#999'            ctx.fillText(footSubTitle, 40, 820)            // 绘制小程序码            const photoPage = await loadImage.call(this, pageCode, 'canvas_share')            ctx.drawImage(photoPage, 510, 730, 150, 150)            // 绘制边框和分割线            ctx.strokeStyle = '#eee'            ctx.lineWidth = 8            ctx.strokeRect(0, 0, width, height)            ctx.lineWidth = 3            ctx.beginPath()            ctx.moveTo(0, 700)            ctx.lineTo(700, 700)            ctx.stroke()            ctx.save()            wx.hideLoading()            // 生成图片预览            wx.canvasToTempFilePath({              x: 0,              y: 0,              width,              height,              canvas,              complete: resTemp => {                console.log('resTemp', canvas, resTemp)                if (resTemp.errMsg === 'canvasToTempFilePath:ok') {                  this.imageShare.image = resTemp.tempFilePath                  this.imageShare.visible = true                  wx.saveImageToPhotosAlbum({                    filePath: resTemp.tempFilePath,                    success () {                      wx.showToast({                        title: '图片曾经保留到相册',                        icon: 'none'                      })                    },                    fail () {                      wx.showToast({                        title: '请先在设置里关上相册权限',                        icon: 'none'                      })                    }                  })                }              }            })          })      },      async getDetail (_id) {        wx.showLoading({          title: '加载中...'        })        const res = await wx.cloud.callFunction({          name: 'answerDetail',          data: {            _id          }        })        this.detail = res.result.data        this.detail.result = this.detail.result.map((e, index) => {          return {            letter: no2letter(this.detail.answer[index]),            right: e          }        })        const OPENID = this.user.OPENID        this.showResult = OPENID === this.detail.creator.OPENID || OPENID === this.detail.questionCreator.OPENID        wx.hideLoading()      },      toCards () {        wx.navigateTo({          url: `detail-cards?_id=${this.detail.question._id}`        })      },      toCreate () {        wx.navigateTo({          url: 'new'        })      },      toDetail () {        wx.navigateTo({          url: `detail-entry?_id=${this.detail.question._id}`        })      }    }  })</script>// 款式省略

谢谢浏览

喜爱我的我的项目,欢送star反对一下