HTML5教程

小程序开发一个朋友圈热门的互动答题应用

本文主要是介绍小程序开发一个朋友圈热门的互动答题应用,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

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

功能

  1. 创建自定义题目,设置正确选项
  2. 生成分享图邀请好友来答题
  3. 好友得到成绩单,并可以生成图片分享
  4. 通知中心可以查看好友答题记录

技术栈

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

展示

小程序搜索:小题卡。由于使用免费到云套餐,接口有访问次数限制

预览地址

小题卡.jpg

截图

应用截图.jpg

代码

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

代码结构

代码结构.jpg

示例

由于完整项目涉及简单交互较多,下面展示一些主要功能代码

新建题目

核心要点:用 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支持一下
这篇关于小程序开发一个朋友圈热门的互动答题应用的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!