main
xy 2 years ago
parent 3d90a51da3
commit 62a20d3e3c

@ -14,6 +14,7 @@ let apiApp = {
pointDetail: '/api/mobile/map-point/point-detail',
share: '/api/mobile/user/wechat-share',
savePoster: '/api/mobile/vr/save-poster',
getPoster: '/api/mobile/vr/get-poster',
}
// 此处第二个参数vm就是我们在页面使用的this你可以通过vm获取vuex等操作
@ -33,10 +34,10 @@ const install = (Vue, vm) => {
let pointDetail = (params = {}) => vm.$u.get(apiApp.pointDetail, params);
let share = (params = {}) => vm.$u.get(apiApp.share, params);
let savePoster = (params = {}) => vm.$u.get(apiApp.savePoster, params);
let getPoster = (params = {}) => vm.$u.get(apiApp.getPoster, params)
let saveUser = (params = {}) => vm.$u.post(apiApp.saveUser, params);
// 将各个定义的接口名称统一放进对象挂载到vm.$u.api(因为vm就是this也即this.$u.api)下
vm.$u.api = { login, getAppId, getQuestions, baseFormIndex, baseFormShow, baseFormSave, baseFormDestroy, getPoints, saveQuiz, pointDetail, share, savePoster, user, saveUser };
vm.$u.api = { login, getAppId, getQuestions, baseFormIndex, baseFormShow, baseFormSave, baseFormDestroy, getPoints, saveQuiz, pointDetail, share, savePoster, user, saveUser, getPoster };
}
export default {

@ -32,6 +32,12 @@
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/sign/sign",
"style": {
"navigationStyle": "custom"
}
}
],
"subPackages": [],

@ -1,6 +1,6 @@
<template>
<div class="body" id="signed">
<u-image class="bkg" width="100vw" height="100vh" :src="require('@/static/certificate-bkg.png')"></u-image>
<u-image class="bkg" width="100vw" height="100%" :src="require('@/static/certificate-bkg.png')"></u-image>
<u-image class="city" mode="widthFix" width="100vw" :src="require('@/static/city.png')"></u-image>
<u-image class="people" mode="widthFix" :width="278" :src="require('@/static/people.png')"></u-image>
<u-image mode="widthFix" width="100vw" class="piaodai" :src="require('@/static/piaodai.png')"></u-image>
@ -14,7 +14,7 @@
<div class="container__text">
<div class="container__text--title">
<span>{{ vuex_user.name ? vuex_user.name : name }}</span>
<u-image :width="100" :src="signInfo.upload ? signInfo.upload.url : ''" mode="heightFix" :height="56"></u-image>
</div>
<div class="container__text--total">
你已完成2023年我是党史记录人红色少年行未成年人研学线上打卡
@ -24,6 +24,8 @@
</div>
</div>
</div>
<u-image class="qrcode" :src="require('@/static/qrcode.png')" width="220" height="220" border-radius="10"></u-image>
</div>
<div class="footer">
@ -54,11 +56,11 @@
</view>
</view>
<u-modal v-model="isShowModal" title="您的名字是?" @confirm="saveName">
<view class="slot-content" style="padding: 10rpx 20rpx;">
<u-input v-model="name"></u-input>
</view>
</u-modal>
<!-- <u-modal v-model="isShowModal" title="您的名字是?" @confirm="saveName">-->
<!-- <view class="slot-content" style="padding: 10rpx 20rpx;">-->
<!-- <u-input v-model="name"></u-input>-->
<!-- </view>-->
<!-- </u-modal>-->
</div>
</template>
@ -71,10 +73,11 @@ import html2canvas from '@/lib/html2canvas.min.js'
export default {
data() {
return {
signInfo: '',
isShare: false,
isHidden: false,
imgData: '',
isShowModal: false,
//isShowModal: false,
name: '',
};
@ -87,6 +90,15 @@ export default {
}))
},
async getPoster () {
const res = await this.$u.api.getPoster({
user_id: this.vuex_user.id,
type: 2
})
this.signInfo = res
console.log(1123,res)
},
share() {
this.isHidden = true
@ -174,9 +186,12 @@ export default {
})
}
},
onShow() {
if (!this.vuex_user.name) {
this.isShowModal = true
async onShow() {
await this.getPoster()
if (!this.signInfo?.upload) {
uni.navigateTo({
url: '/pages/sign/sign'
})
}
}
}
@ -307,6 +322,8 @@ export default {
padding: 61rpx 63rpx 0 104rpx;
position: relative;
&--title {
display: flex;
align-items: center;
font-weight: 400;
color: #6F6F6F;
font-size: 28rpx;
@ -339,6 +356,11 @@ export default {
}
}
}
.qrcode {
margin: 150rpx auto;
}
}
.share_cover {

@ -0,0 +1,377 @@
<template>
<view class="sign">
<view class="sign-bkg">
<view class="sign-bkg__word">
<span></span>
<span></span>
</view>
</view>
<view class="sign-pic">
<!-- <image src="/static/cloud.png" class="sign-pic__cloud" mode="widthFix"></image>-->
<image class="sign-pic__city" :src="require('@/static/city.png')" mode="widthFix" />
</view>
<view class="sign-cnv">
<l-signature disableScroll backgroundColor="#ddd" ref="signatureRef" :penColor="options.penColor"
:backgroundColor="options.backgroundColor" :penSize="options.penSize" :openSmooth="options.openSmooth">
</l-signature>
</view>
<view class="sign-btn">
<view class="sign-btn__reset" @click="$u.throttle(onClick('clear'))"></view>
<view class="sign-btn__confirm" @click="$u.throttle(onClick('save'))"></view>
</view>
</view>
</template>
<script>
import {
base64ToFile
} from '@/common/util.js'
import {
ROOTPATH
} from '@/common/config.js'
export default {
data() {
return {
url: '',
options: {
penColor: '#000',
penSize: 16,
openSmooth: true,
backgroundColor: 'rgba(0,0,0,0)'
},
}
},
methods: {
//src - deg
rotateBase64Img(src, edg) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let imgW //
let imgH //
let size // canvas
if (edg % 90 != 0) {
console.error('旋转角度必须是90的倍数!')
}
(edg < 0) && (edg = (edg % 360) + 360)
const quadrant = (edg / 90) % 4 //
const cutCoor = {
sx: 0,
sy: 0,
ex: 0,
ey: 0
} //
const image = new Image()
image.crossOrigin = 'anonymous'
image.src = src
image.onload = () => {
imgW = image.width
imgH = image.height
size = imgW > imgH ? imgW : imgH
canvas.width = size * 2
canvas.height = size * 2
switch (quadrant) {
case 0:
cutCoor.sx = size
cutCoor.sy = size
cutCoor.ex = size + imgW
cutCoor.ey = size + imgH
break
case 1:
cutCoor.sx = size - imgH
cutCoor.sy = size
cutCoor.ex = size
cutCoor.ey = size + imgW
break
case 2:
cutCoor.sx = size - imgW
cutCoor.sy = size - imgH
cutCoor.ex = size
cutCoor.ey = size
break
case 3:
cutCoor.sx = size
cutCoor.sy = size - imgW
cutCoor.ex = size + imgH
cutCoor.ey = size + imgW
break
}
ctx.translate(size, size)
ctx.rotate(edg * Math.PI / 180)
ctx.drawImage(image, 0, 0)
var imgData = ctx.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey)
if (quadrant % 2 == 0) {
canvas.width = imgW
canvas.height = imgH
} else {
canvas.width = imgH
canvas.height = imgW
}
ctx.putImageData(imgData, 0, 0)
// base64
resolve(canvas.toDataURL())
}
})
},
onClick(type) {
if (type == 'openSmooth') {
this.openSmooth = !this.openSmooth
return
}
if (type == 'save') {
this.$refs.signatureRef.canvasToTempFilePath({
success: (res) => {
//
if (res.isEmpty) {
uni.showToast({
title: '请签名',
icon: 'none'
})
return
}
//
// app | H5 | base64
if (res.tempFilePath) {
//this.$u.vuex('vuex_sign_base64', res.tempFilePath)
this.rotateBase64Img(res.tempFilePath, -90).then(res => {
this.$u.vuex('vuex_sign_base64', res)
let form = new FormData()
form.append('file', base64ToFile(res, 'sign'))
form.append('active_tag', 'map_point')
form.append('activity_list_id', 7)
form.append('token',this.vuex_token)
let xhr = new XMLHttpRequest()
xhr.open('post', `${ROOTPATH}/api/mobile/upload-file`)
xhr.onreadystatechange = () => {
if (xhr.status === 200 && xhr.readyState ===
4) {
let val = JSON.parse(xhr.responseText);
console.log(val);
this.$u.api.savePoster({
upload_id: val.id,
type: 2
}).then(_ => {
uni.navigateTo({
url: `/pages/certificate/certificate`
})
})
}
}
xhr.send(form)
})
}
}
})
return
}
if (this.$refs.signatureRef)
this.$refs.signatureRef[type]()
},
},
}
</script>
<style scoped lang="scss">
@media screen and (orientation: portrait) {
.sign {
height: 100vh;
width: 100vw;
&-pic {
width: 100vh;
display: flex;
align-items: flex-end;
transform: rotate(90deg) translateX(0%) translateY(-100%);
transform-origin: 0 0;
position: absolute;
&__cloud {
flex: 1;
}
&__city {
flex: 1;
}
}
&-bkg {
background-color: #FEFAEE;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
&__word {
width: 390rpx;
font-size: 128rpx;
font-weight: 400;
color: #BBB6B2;
opacity: 0.3;
display: flex;
justify-content: space-between;
transform: rotate(90deg) translateX(-50%) translateY(calc(-100% - 426rpx));
transform-origin: 0 0;
position: absolute;
top: 50%;
}
}
&-cnv {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
&-btn {
display: flex;
transform: rotate(90deg) translateX(-50%) translateY(-100%);
transform-origin: 0% 0%;
margin-left: 112rpx;
z-index: 99999;
position: fixed;
top: 50%;
&__reset {
width: 200rpx;
height: 66rpx;
background: #FEFAEE;
border: 4rpx solid #CA3F32;
border-radius: 34rpx;
font-size: 26rpx;
font-weight: 400;
color: #B61F2E;
line-height: 58rpx;
text-align: center;
margin-right: 52rpx;
}
&__confirm {
font-size: 26rpx;
font-weight: 400;
color: #FFFFFF;
width: 200rpx;
height: 66rpx;
background: linear-gradient(0deg, #E16055, #C54439);
border-radius: 34rpx;
line-height: 66rpx;
text-align: center;
}
}
}
}
@media screen and (orientation: landscape) {
.sign {
height: 100vh;
width: 100vw;
&-pic {
width: 100vw;
display: flex;
align-items: flex-end;
position: absolute;
bottom: 0;
&__cloud {
flex: 1;
}
&__city {
flex: 1;
}
}
&-bkg {
background-color: #FEFAEE;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
&__word {
width: 18vw;
font-size: 6vw;
font-weight: 400;
color: #BBB6B2;
opacity: 0.3;
display: flex;
justify-content: space-between;
transform: translate(-50%, -50%);
position: absolute;
top: 50%;
left: 50%;
}
}
&-cnv {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
&-btn {
display: flex;
transform: translateX(-50%);
z-index: 99999;
position: fixed;
bottom: 8vh;
left: 50%;
&__reset {
width: 16.6vw;
height: 5.2vw;
background: #FEFAEE;
border: 0.4vw solid #CA3F32;
border-radius: 34rpx;
font-size: 2.6vw;
font-weight: 400;
color: #B61F2E;
text-align: center;
line-height: 5.2vw;
box-sizing: content-box;
margin-right: 48rpx;
}
&__confirm {
font-size: 2.6vw;
font-weight: 400;
color: #FFFFFF;
width: 17.4vw;
height: 6vw;
background: linear-gradient(0deg, #E16055, #C54439);
border-radius: 34rpx;
text-align: center;
line-height: 6vw;
}
}
}
}
</style>

@ -172,7 +172,7 @@
var html=" ";
function showVideo(obj){
var videourl = $(obj).attr("videourl");
html = "<video src='"+videourl+"' autoplay muted preload='auto' controls></video>";
html = "<video src='"+videourl+"' autoplay preload='auto' controls></video>";
$(".videobox").html(html);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

@ -36,6 +36,7 @@ const store = new Vuex.Store({
vuex_token: lifeData.vuex_token ? lifeData.vuex_token : '',
// 如果vuex_version无需保存到本地永久存储无需lifeData.vuex_version方式
vuex_version: '1.0.0',
vuex_sign_base64: '',
vuex_point_id: '',
},

@ -0,0 +1,22 @@
## 1.0.02022-10-27
- feat: 增加背景色
- feat: 修复 app canvasToTempFilePath 无操作只能执行一次的问题
## 0.8.02022-08-22
- feat: 增加beforeDelay 延时初始化,可用于手写板在弹窗里时
## 0.7.02022-08-16
- fix: 修复缺少 canvasWidth
## 0.6.02022-07-16
- fix: 修复 success is no defined
## 0.5.02022-07-09
- feat: canvasToTempFilePath success 增加返回 isEmpty
- fix: 修复 微信小程序 canvasToTempFilePath 无效问题
## 0.4.02022-07-04
- fix: 生成图片缺少最后一笔
## 0.3.02022-05-24
- chore: 支持多端 H5 小程序 APP APP-NVUE
## 0.2.02021-07-09
- chore: 统一命名规范,无须主动引入组件
- fix: 修复错位问题
## 0.1.02021-03-07
- 首次上传
- 撤消、清空、保存、模拟压感等功能

@ -0,0 +1,66 @@
export const uniContext = (ctx) => {
const ALIAS_ATTRS_MAP = [
'lineCap',
'strokeStyle',
'lineWidth',
'fillStyle',
]
ALIAS_ATTRS_MAP.forEach(style => {
Object.defineProperty(ctx, style, {
set: value => {
if(value)
ctx[`set${style.charAt(0).toUpperCase()}${style.slice(1)}`](value)
}
})
})
ctx.uniDrawImage = ctx.drawImage
ctx.drawImage = (image,...agrs) => {
ctx.uniDrawImage(image.src, ...agrs)
}
return ctx
}
class Image {
constructor() {
this.currentSrc = null
this.naturalHeight = 0
this.naturalWidth = 0
this.width = 0
this.height = 0
this.tagName = 'IMG'
}
set src(src) {
this.currentSrc = src
uni.getImageInfo({
src,
success: (res) => {
this.naturalWidth = this.width = res.width
this.naturalHeight = this.height = res.height
this.onload()
},
fail: () => {
this.onerror()
}
})
}
get src() {
return this.currentSrc
}
}
export const createImage = () => {
return new Image()
}
export const toDataURL = (canvasId, com) => {
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvasId,
success: (res) => {
resolve(res.tempFilePath)
},
fail: reject
}, com)
})
}

@ -0,0 +1,641 @@
<template>
<view class="lime-signature" :style="[canvasStyle, styles]" ref="limeSignature">
<!-- #ifndef APP-VUE || APP-NVUE -->
<canvas v-if="useCanvas2d" class="lime-signature__canvas" :id="canvasId" type="2d"
:disableScroll="disableScroll" @touchstart="touchStart" @touchmove="touchMove"
@touchend="touchEnd"></canvas>
<canvas v-else :disableScroll="disableScroll" class="lime-signature__canvas" :canvas-id="canvasId"
:id="canvasId" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" @mousedown="touchStart"
@mousemove="touchMove" @mouseup="touchEnd"></canvas>
<!-- #endif -->
<!-- #ifdef APP-VUE -->
<view :id="canvasId" :disableScroll="disableScroll" :rparam="param" :change:rparam="sign.update"
:rclear="rclear" :change:rclear="sign.clear" :rundo="rundo" :change:rundo="sign.undo" :rsave="rsave"
:change:rsave="sign.save" :rempty="rempty" :change:rempty="sign.isEmpty"></view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<web-view src="/uni_modules/lime-signature/static/index.html" class="lime-signature__canvas" ref="webview"
@pagefinish="onPageFinish" @error="onError" @onPostMessage="onMessage"></web-view>
<!-- #endif -->
</view>
</template>
<!-- #ifdef APP-VUE -->
<script module="sign" lang="renderjs">
// #ifdef APP-VUE
// import { Signature } from '@signature'
import {
Signature
} from './signature'
// import {base64ToPath} from './utils'
export default {
data() {
return {
canvasid: null,
signature: null,
observer: null,
options: {},
saveCount: 0,
}
},
mounted() {
this.$nextTick(this.init)
},
methods: {
init() {
const el = this.$refs.limeSignature;
const canvas = document.createElement('canvas')
canvas.style = 'width:100%; height: 100%;'
el.appendChild(canvas)
this.signature = new Signature({
el: canvas
})
this.signature.pen.setOption(this.options)
const width = this.signature.canvas.get('width')
const height = this.signature.canvas.get('height')
this.emit({
changeSize: {
width,
height
}
})
},
undo(v) {
if (v && this.signature) {
this.signature.undo()
}
},
clear(v) {
if (v && this.signature) {
this.signature.clear()
}
},
save(v) {
if (v !== this.saveCount) {
this.saveCount = v;
const image = this.signature.canvas.get('el').toDataURL()
const {
backgroundColor
} = this.options
if (backgroundColor) {
const canvas = document.createElement('canvas')
const width = this.signature.canvas.get('width')
const height = this.signature.canvas.get('height')
const pixelRatio = this.signature.canvas.get('pixelRatio')
canvas.width = width * pixelRatio
canvas.height = height * pixelRatio
const context = canvas.getContext('2d')
context.scale(pixelRatio, pixelRatio)
context.fillStyle = backgroundColor
context.fillRect(0, 0, width, height)
context.drawImage(this.signature.canvas.get('el'), 0, 0, width, height)
this.emit({
save: canvas.toDataURL()
})
canvas.remove()
} else {
this.emit({
save: image
})
}
// base64ToPath(image).then((res) => {
// this.emit({save: res})
// })
}
},
isEmpty(v) {
if (v && this.signature) {
const isEmpty = this.signature.isEmpty()
this.emit({
isEmpty
})
}
},
emit(event) {
this.$ownerInstance.callMethod('onMessage', {
detail: {
data: [{
event
}]
}
})
},
update(v) {
if (v) {
if (this.signature) {
this.options = v
this.signature.pen.setOption(v)
} else {
this.options = v
}
}
}
}
}
// #endif
</script>
<!-- #endif -->
<script>
// #ifndef APP-NVUE
import {
getCanvas2d,
wrapEvent,
requestAnimationFrame,
sleep
} from './utils'
import {
Signature
} from './signature'
// import {Signature} from '@signature';
import {
uniContext,
createImage,
toDataURL
} from './context'
// #endif
import {
base64ToPath
} from './utils'
export default {
props: {
styles: String,
disableScroll: Boolean,
type: {
type: String,
default: '2d'
},
//
penColor: {
type: String,
default: 'black'
},
penSize: {
type: Number,
default: 2
},
//
backgroundColor: String,
//
openSmooth: Boolean,
//
minLineWidth: {
type: Number,
default: 2
},
//
maxLineWidth: {
type: Number,
default: 6
},
// (px/ms)1.0-10.0
minSpeed: {
type: Number,
default: 1.5
},
// 线()1-100线使maxWidthDiffRate
maxWidthDiffRate: {
type: Number,
default: 20
},
// 0
maxHistoryLength: {
type: Number,
default: 20
},
beforeDelay: {
type: Number,
default: 0
}
},
data() {
return {
canvasWidth: null,
canvasHeight: null,
useCanvas2d: true,
// #ifdef APP-PLUS
rclear: 0,
rundo: 0,
rsave: 0,
rempty: 0,
risEmpty: true,
toDataURL: null,
tempFilePath: [],
// #endif
}
},
computed: {
canvasId() {
return `lime-signature${this._uid||this._.uid}`
},
canvasStyle() {
const {
canvasWidth,
canvasHeight,
backgroundColor
} = this
return {
width: canvasWidth && (canvasWidth + 'px'),
height: canvasHeight && (canvasHeight + 'px'),
background: backgroundColor
}
},
param() {
const {
penColor,
penSize,
backgroundColor,
openSmooth,
minLineWidth,
maxLineWidth,
minSpeed,
maxWidthDiffRate,
maxHistoryLength,
disableScroll
} = this
return JSON.parse(JSON.stringify({
penColor,
penSize,
backgroundColor,
openSmooth,
minLineWidth,
maxLineWidth,
minSpeed,
maxWidthDiffRate,
maxHistoryLength,
disableScroll
}))
}
},
// #ifdef APP-NVUE
watch: {
param(v) {
this.$refs.webview.evalJS(`update(${JSON.stringify(v)})`)
}
},
// #endif
// #ifndef APP-PLUS
created() {
this.useCanvas2d = this.type === '2d' && getCanvas2d()
},
// #endif
// #ifndef APP-PLUS
async mounted() {
if (this.beforeDelay) {
await sleep(this.beforeDelay)
}
const config = await this.getContext()
this.signature = new Signature(config)
this.canvasEl = this.signature.canvas.get('el')
this.canvasWidth = this.signature.canvas.get('width')
this.canvasHeight = this.signature.canvas.get('height')
this.stopWatch = this.$watch('param', (v) => {
this.signature.pen.setOption(v)
}, {
immediate: true
})
},
// #endif
// #ifndef APP-PLUS
// #ifdef VUE3
beforeUnmount() {
this.stopWatch()
this.signature.destroy()
},
// #endif
// #ifdef VUE2
beforeDestroy() {
this.stopWatch()
this.signature.destroy()
},
// #endif
// #endif
methods: {
// #ifdef APP-PLUS
onPageFinish() {
this.$refs.webview.evalJS(`update(${JSON.stringify(this.param)})`)
},
onMessage(e = {}) {
const {
detail: {
data: [res]
}
} = e
if (res.event?.save) {
this.toDataURL = res.event.save
}
if (res.event?.changeSize) {
const {
width,
height
} = res.event.changeSize
}
if (res.event.hasOwnProperty('isEmpty')) {
this.risEmpty = res.event.isEmpty
}
if (res.event?.file) {
this.tempFilePath.push(res.event.file)
if (this.tempFilePath.length > 7) {
this.tempFilePath.shift()
}
return
}
if (res.event?.success) {
if (res.event.success) {
this.tempFilePath.push(res.event.success)
if (this.tempFilePath.length > 8) {
this.tempFilePath.shift()
}
this.toDataURL = this.tempFilePath.join('')
this.tempFilePath = []
// base64ToPath(this.tempFilePath.join('')).then(res => {
// })
} else {
this.$emit('fail', 'canvas no data')
}
return
}
},
// #endif
undo() {
// #ifdef APP-VUE || APP-NVUE
this.rundo += 1
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJS(`undo()`)
// #endif
// #ifndef APP-VUE
if (this.signature)
this.signature.undo()
// #endif
},
clear() {
// #ifdef APP-VUE || APP-NVUE
this.rclear += 1
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJS(`clear()`)
// #endif
// #ifndef APP-VUE
if (this.signature)
this.signature.clear()
// #endif
},
isEmpty() {
// #ifdef APP-NVUE
this.$refs.webview.evalJS(`isEmpty()`)
// #endif
// #ifdef APP-VUE || APP-NVUE
this.rempty += 1
// #endif
// #ifndef APP-VUE || APP-NVUE
return this.signature.isEmpty()
// #endif
},
canvasToTempFilePath(param) {
const isEmpty = this.isEmpty()
// #ifdef APP-NVUE
this.$refs.webview.evalJS(`save()`)
// #endif
// #ifdef APP-VUE || APP-NVUE
const stopURLWatch = this.$watch('toDataURL', (v, n) => {
if (v && v !== n) {
if (param.pathType == 'url') {
base64ToPath(v).then(res => {
param.success({
tempFilePath: res,
isEmpty: this.risEmpty
})
})
} else {
param.success({
tempFilePath: v,
isEmpty: this.risEmpty
})
}
this.toDataURL = ''
}
stopURLWatch && stopURLWatch()
})
this.rsave += 1
// #endif
// #ifndef APP-VUE || APP-NVUE
const success = (success) => param.success && param.success(success)
const fail = (fail) => param.fail && param.fail(err)
const {
canvas
} = this.signature.canvas.get('el')
const context = this.signature.canvas.get('context')
const {
backgroundColor
} = this
const width = this.signature.canvas.get('width')
const height = this.signature.canvas.get('height')
if (this.useCanvas2d) {
try {
// #ifndef MP-ALIPAY
const tempFilePath = canvas.toDataURL()
if (backgroundColor) {
const image = canvas.createImage()
image.src = tempFilePath
image.onload = () => {
context.fillStyle = backgroundColor
context.fillRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
const tempFilePath = canvas.toDataURL()
success({
tempFilePath,
isEmpty
})
context.clearRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
}
} else {
success({
tempFilePath,
isEmpty
})
}
// #endif
// #ifdef MP-ALIPAY
canvas.toTempFilePath({
canvasid: this.canvasid,
success(res) {
if (backgroundColor) {
const image = canvas.createImage()
image.src = tempFilePath
image.onload = () => {
canvas.toTempFilePath({
canvasid: this.canvasid,
success(res) {
context.fillStyle = backgroundColor
context.fillRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
success({
tempFilePath,
isEmpty
})
context.clearRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
}
})
}
} else {
success({
tempFilePath: res,
isEmpty
})
}
},
fail
})
// #endif
} catch (err) {
console.warn(err)
fail(err)
}
} else {
toDataURL(this.canvasId, this).then(res => {
if (backgroundColor) {
const image = createImage()
image.src = res
image.onload = () => {
context.fillStyle = backgroundColor
context.fillRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
context.draw && context.draw(true, () => {
toDataURL(this.canvasId, this).then(res => {
success({
tempFilePath: res,
isEmpty
})
context.clearRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
context.draw && context.draw(true)
})
});
}
} else {
success({
tempFilePath: res,
isEmpty
})
}
}).catch(err => {
console.warn(err)
fail(err)
})
}
// #endif
},
// #ifndef APP-PLUS
getContext() {
const {
pixelRatio
} = uni.getSystemInfoSync()
return new Promise(resolve => {
if (this.useCanvas2d) {
uni.createSelectorQuery().in(this)
.select(`#${this.canvasId}`)
.fields({
node: true,
size: true,
rect: true,
})
.exec(res => {
if (res) {
const {
width,
height,
node,
left,
top,
right
} = res[0]
const context = node.getContext('2d')
node.width = width * pixelRatio;
node.height = height * pixelRatio;
resolve({
left,
top,
right,
width,
height,
context,
canvas: node,
pixelRatio
})
}
})
} else {
uni.createSelectorQuery().in(this)
.select(`#${this.canvasId}`)
.boundingClientRect()
.exec(res => {
if (res) {
const {
width,
height,
left,
top,
right
} = res[0]
const context = uniContext(uni.createCanvasContext(this.canvasId, this))
const canvas = {
createImage,
toDataURL: () => toDataURL(this.canvasId, this),
requestAnimationFrame
}
resolve({
left,
top,
right,
width,
height,
context,
pixelRatio: 1,
canvas
})
}
})
}
})
},
touchStart(e) {
if (!this.canvasEl) return
this.isStart = true
this.canvasEl.dispatchEvent('touchstart', wrapEvent(e))
},
touchMove(e) {
if (!this.canvasEl || !this.isStart && this.canvasEl) return
this.canvasEl.dispatchEvent('touchmove', wrapEvent(e))
},
touchEnd(e) {
if (!this.canvasEl) return
this.isStart = false
this.canvasEl.dispatchEvent('touchend', wrapEvent(e))
},
// #endif
}
}
</script>
<style lang="stylus">
.lime-signature,
.lime-signature__canvas {
// #ifndef APP-NVUE
width: 100%;
height: 100% // #endif
// #ifdef APP-NVUE
flex: 1;
// #endif}
</style>

File diff suppressed because one or more lines are too long

@ -0,0 +1,95 @@
export function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
export const getCanvas2d = () => {
let {SDKVersion, uniPlatform} = uni.getSystemInfoSync()
if(!uniPlatform) {
// #ifdef MP-WEIXIN
uniPlatform = 'mp-weixin'
// #endif
// #ifdef MP-MP-ALIPAY
SDKVersion = my.SDKVersion
uniPlatform = 'mp-alipay'
// #endif
// #ifdef MP-MP-ALIPAY
uniPlatform = 'mp-toutiao'
// #endif
}
const MAP = {
'mp-weixin': '2.9.7',
'mp-toutiao': '1.78.0',
'mp-alipay': '2.7.0'
}[uniPlatform]
return MAP && SDKVersion && compareVersion(SDKVersion, MAP) >= 1
}
export const wrapEvent = (e) => {
if (!e) return;
if (!e.preventDefault) {
e.preventDefault = function() {};
}
return e;
}
export const requestAnimationFrame = (cb) => {
setTimeout(cb, 30)
}
/**
* base64转路径
* @param {Object} base64
*/
export function base64ToPath(base64) {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
return new Promise((resolve, reject) => {
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
bitmap.loadBase64Data(base64, () => {
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
const filePath = `_doc/uniapp_temp/${time}.${format}`
bitmap.save(filePath, {},
() => {
bitmap.clear()
resolve(filePath)
},
(error) => {
bitmap.clear()
reject(error)
})
}, (error) => {
bitmap.clear()
reject(error)
})
})
}
export function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay))
}

@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>

@ -0,0 +1,76 @@
{
"id": "lime-signature",
"displayName": "手写板-签名",
"version": "1.0.0",
"description": "手写板签名组件: 一款能跑在uniapp各端中的签名插件支持签名颜色笔画大小等功能",
"keywords": [
"canvas",
"写字",
"签名",
"涂鸦"
],
"repository": "",
"engines": {
"HBuilderX": "^3.5.4"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "u",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,100 @@
# signature 写字板
> uniapp 写字板,可用业务签名等场景
> [查看更多 站点1](https://limeui.qcoon.cn/#/signature) <br>
> [查看更多 站点2](http://liangei.gitee.io/limeui/#/signature)
> Q群1169785031
## 平台兼容
| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- |
| √ | √ | 未测 | 未测 | 未测 | 未测 | √ |
## 代码演示
### 基本用法
```html
<view style="width: 750rpx ;height: 750rpx;">
<l-signature disableScroll backgroundColor="#ddd" ref="signatureRef" :penColor="penColor" :penSize="penSize" :openSmooth="openSmooth" ></l-signature>
</view>
<view>
<button @click="onClick('clear')">清空</button>
<button @click="onClick('undo')">撤消</button>
<button @click="onClick('save')">保存</button>
<button @click="onClick('openSmooth')">压感{{openSmooth?'开':'关'}}</button>
</view>
```
```js
export default {
data() {
return {
title: 'Hello',
penColor: 'red',
penSize: 5,
url: '',
openSmooth: true
}
},
methods: {
onClick(type) {
if(type == 'openSmooth') {
this.openSmooth = !this.openSmooth
return
}
if (type == 'save') {
this.$refs.signatureRef.canvasToTempFilePath({
success: (res) => {
// 是否为空画板 无签名
console.log(res.isEmpty)
// 生成图片的临时路径
// app | H5 | 微信小程序 生成的是base64
this.url = res.tempFilePath
}
})
return
}
if (this.$refs.signatureRef)
this.$refs.signatureRef[type]()
}
}
}
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| -------------- | ------------ | ---------------- | ------------ |
| penSize | 画笔大小 | <em>number</em> | `2` |
| minLineWidth | 线条最小宽 | <em>number</em> | `2` |
| maxLineWidth | 线条最大宽 | <em>number</em> | `6` |
| penColor | 画笔颜色 | <em>string</em> | `black` |
| backgroundColor | 背景颜色 | <em>string</em> | `` |
| type | 指定 canvas 类型 | <em>string</em> | `2d` |
| openSmooth | 是否模拟压感 | <em>boolean</em> | `false` |
| beforeDelay | 延时初始化,在放在弹窗里可以使用 (毫秒) | <em>number</em> | `0` |
| maxHistoryLength | 限制历史记录数即最大可撤销数传入0则关闭历史记录功能 | <em>boolean</em> | `20` |
### 事件 Events
| 事件名 | 说明 | 回调 |
| ------- | ------------ | -------------- |
| undo | 撤消,回退到上一步 | |
| clear | 清空,清空画板 | |
| canvasToTempFilePath | 保存生成图片与官方保持一致但不需要传canvasId | |
### 常见问题
- 放在弹窗里时,尺寸不对 可以延时手写板出现时机给手写板加vif或beforeDelay="300"
### 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![输入图片说明](https://static-6d65bd90-8508-4d6c-abbc-a4ef5c8e49e7.bspapp.com/image/222521_bb543f96_518581.jpeg "微信图片编辑_20201122220352.jpg")
![输入图片说明](https://static-6d65bd90-8508-4d6c-abbc-a4ef5c8e49e7.bspapp.com/image/wxplay.jpg "wxplay.jpg")

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title></title>
<style type="text/css">
html,
body,
canvas {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
overflow-y: hidden;
background-color: transparent;
}
</style>
</head>
<body>
<canvas id="lime-signature"></canvas>
<script type="text/javascript" src="./uni.webview.1.5.3.js"></script>
<script type="text/javascript" src="./signature.js"></script>
<script>
var signature = null;
var timer = null;
var isStart = false;
var options = null
console.log = function(...args) {
postMessage(args);
};
// function stringify(key, value) {
// if (typeof value === 'object' && value !== null) {
// if (cache.indexOf(value) !== -1) {
// return;
// }
// cache.push(value);
// }
// return value;
// };
function emit(event, data) {
postMessage({
event,
data: typeof data !== "object" && data !== null ? data : JSON.stringify(data),
});
// cache = [];
}
function postMessage(data) {
uni.postMessage({
data
});
}
function update(v = {}) {
if (signature) {
options = v
signature.pen.setOption(v);
} else {
signature = new Signature.Signature({el: "lime-signature"});
canvasEl = signature.canvas.get("el");
options = v
signature.pen.setOption(v)
const width = signature.canvas.get("width");
const height = signature.canvas.get("height");
emit({changeSize: {width,height}})
}
}
function clear() {
signature.clear()
}
function undo() {
signature.undo()
}
function isEmpty() {
const isEmpty = signature.isEmpty()
emit({isEmpty});
}
function save(args) {
// delete args.success;
// delete args.fail;
clearTimeout(timer);
timer = setTimeout(() => {
let path = canvasEl.toDataURL()
if(options.backgroundColor) {
const canvas = document.createElement('canvas')
const width = signature.canvas.get('width')
const height = signature.canvas.get('height')
const pixelRatio = signature.canvas.get('pixelRatio')
canvas.width = width * pixelRatio
canvas.height = height * pixelRatio
const context = canvas.getContext('2d')
context.scale(pixelRatio, pixelRatio)
context.fillStyle = backgroundColor
context.fillRect(0,0, width, height)
context.drawImage(signature.canvas.get('el'), 0, 0, width, height)
path = canvas.toDataURL()
canvas.remove()
}
if (typeof path == "string") {
const index = Math.ceil(path.length / 8);
for (var i = 0; i < 8; i++) {
if (i == 7) {
emit({"success": path.substr(i * index, index)});
} else {
emit({"file": path.substr(i * index, index)});
}
}
} else {
console.error("canvas no data");
emit({"fail": "canvas no data"});
}
}, 30);
}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save