这是之前受到朋友圈的一些 亲密度测试 的启发,开发的一个互动答题应用。
功能
- 创建自定义题目,设置正确选项
- 生成分享图邀请好友来答题
- 好友得到成绩单,并可以生成图片分享
- 通知中心可以查看好友答题记录
技术栈
- 前端:小程序
- 后端:云开发
- 框架:mpx
展示
预览地址
截图
代码
git仓库:github.com/luosijie/mi…
代码结构
示例
新建题目
<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>
// 省略样式
成绩单
<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>
// 样式省略
谢谢阅读
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!