这是之前受到朋友圈的一些 亲密度测试 的启发,开发的一个互动答题应用。
小程序搜索:小题卡。由于使用免费到云套餐,接口有访问次数限制
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支持一下