修改签名样式,兼容小程序

dev
liy 2024-08-28 16:29:32 +08:00
parent c6b9bd26cc
commit 4f92c0a473
32 changed files with 1080 additions and 1314 deletions

View File

@ -1,61 +1,129 @@
<template>
<u-modal
:show="signShow"
title="签名"
showCancelButton
confirmText="完成"
cancelText="重写"
@confirm="confirm"
@cancel="clear"
:show-confirm-button="false"
class="esign_dialog"
width="100vw"
>
<view class="handCenter">
<l-signature disableScroll backgroundColor="#fff" ref="signatureRef" :penSize="5"
:openSmooth="true" :landscape="true"></l-signature>
<view class="footer">
<view class="button" @click="fnReset">
<view></view>
<view></view>
</view>
<view class="button" @click="fnGenerate">
<view></view>
<view></view>
</view>
</view>
<view v-if="signShow" class="content">
<sp-sign-board
ref="signRef"
horizontal
bg-color="#fff"
:need-back="false"
/>
</view>
<view class="header">
<view></view>
<view>
<view class="title"></view>
<view class="title"></view>
</view>
<view>
<u-icon name="close" size="26" color="#000" @click="fnClose" />
</view>
</view>
</u-modal>
</template>
<script>
export default {
props: {
signShow: {
type: Boolean,
default: false
<script >
export default {
props: {
signShow: {
type: Boolean,
default: false
},
},
},
methods: {
clear() {
this.$refs.signatureRef.clear()
data() {
return {
visible:false,
}
},
confirm() {
this.$refs.signatureRef.canvasToTempFilePath({
fileType: 'jpg',
quality: 1,
destWidth: 900,
destHeight: 435,
success: (res) => {
if (res.isEmpty) {
uni.showToast({
title: '请签字',
icon: 'none'
})
} else {
this.clear()
this.$emit("update:signShow", false);
this.$emit('confirm', {id: '', filePath: res.tempFilePath})
}
}
})
methods: {
fnReset() {
this.$refs.signRef?.reset && this.$refs.signRef.reset();
},
fnGenerate() {
this.$refs.signRef.confirm();
},
fnClose() {
this.fnReset();
this.$emit("update:signShow", false);
}
},
mounted() {
uni.$on("getSignImg", ({ base64, path }) => {
console.log({ base64, path })
this.$emit('confirm', {id: '', filePath: base64});
this.fnClose();
});
},
beforeDestroy() {
uni.$off("getSignImg");
}
}
}
</script>
<style lang="scss" scoped>
.handCenter {
border: 4rpx dashed #e9e9e9;
height: 80vh;
overflow: hidden;
box-sizing: border-box;
<style scoped lang="scss">
.header {
padding: 50rpx 0;
width: 80rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
text-align: center;
font-size: 40rpx;
color: #000;
margin-right: -25rpx;
.title {
margin-top: 10rpx;
transform: rotate(90deg);
}
}
.content {
flex: 1;
//display: flex;
//align-items: center;
//justify-content: center;
height: 95vh;
border: 1rpx dashed #dcdee0;
}
.footer {
display: flex;
flex-direction: column;
margin-left: -25rpx;
.button {
border: 0;
border-radius: 0;
flex: 1;
border-top: 1rpx solid #dcdee0;
display: block;
padding-top: 25vh;
padding-left: 14rpx;
padding-right: 30rpx;
&:first-child {
border: 0;
}
view {
transform: rotate(90deg);
}
}
}
</style>

View File

@ -145,33 +145,33 @@
</u-cell>
</u-cell-group>
</view>
<view class="card" v-for="(item,index) in hiddenExamineList" :key="index">
<view class="view-title">
<u--text text="确认信息" bold v-if="item.TYPE === 4"></u--text>
@ -278,38 +278,38 @@
</u-cell>
</u-cell-group>
</view>
<view class="card" v-if="form.STATE ==2 || form.STATE ==4 || form.STATE == 10">
<view class="view-title">
<u--text text="整改信息" bold></u--text>
@ -408,35 +408,35 @@
</template>
</u-cell-group>
</view>
<view class="card" v-if="form.STATE ==4">
<view class="view-title">
<u--text text="验收信息" bold></u--text>
@ -566,5 +566,7 @@ export default {
</script>
<style scoped>
.title {
min-width: 150rpx;
}
</style>

View File

@ -198,5 +198,7 @@ export default {
</script>
<style scoped>
.title {
min-width: 150rpx;
}
</style>

View File

@ -329,5 +329,7 @@ export default {
</script>
<style scoped>
.title {
min-width: 150rpx;
}
</style>

View File

@ -9,7 +9,9 @@
clearable
shape="circle"
></u--input>
<u-button class="bth-mini ml-10" type="success" text="确定" @click="resetList"></u-button>
<view class="ml-10">
<u-button class="bth-mini" type="success" text="确定" @click="resetList"></u-button>
</view>
</view>
<u-list @scrolltolower="scrolltolower" v-if="list.length > 0">
<u-list-item v-for="(item, index) in list" :key="index">
@ -31,7 +33,7 @@
</view>
<view class="flex-between mt-10 subtitle">
<text>在线视频数{{item.ONLINE_COUNT}}</text>
<u-button type="primary" text="进入" size="mini" class="bth-mini" @click="fnNavigatorDetail(item.CORPINFO_ID)"/>
<view class="wrap"><u-button type="primary" text="进入" size="mini" class="bth-mini" @click="fnNavigatorDetail(item.CORPINFO_ID)"/></view>
</view>
<!-- <view class="flex-between mt-10 subtitle">-->
<!-- <text>重点工程数{{ item.OUTSOURCED_COUNT }}</text>-->
@ -134,4 +136,8 @@ export default {
right: 0;
transform: skew(5deg); /* 调整角标的位置使其倾斜,以达到更好的效果 */
}
.wrap{
width: 200rpx;
margin: 0 10rpx;
}
</style>

View File

@ -134,7 +134,7 @@
<uni-th align="center">隐患描述</uni-th>
<uni-th align="center">操作</uni-th>
</uni-tr>
<uni-tr v-for="(item,index) in form.hiddenList" :key="item.HIDDEN_ID || item.id">
<uni-tr v-for="(item,index) in form.hiddenList" :key="item.id">
<uni-td>{{ index + 1 }}</uni-td>
<uni-td>{{ item.HIDDENPART_NAME?item.HIDDENPART_NAME:item.HIDDENPART }}</uni-td>
<uni-td>{{ item.HIDDENDESCR }}</uni-td>
@ -188,13 +188,20 @@
<u-cell v-if="form.INSPECTION_USER_OPINION" class="flex-none">
<view slot="title" class="title flex-between">
签字
<u-button type="primary" text="手写添加" size="mini" class="bth-mini" @click="signShow = true"></u-button>
<view>
<u-button type="primary" text="手写添加" size="mini" class="bth-mini" @click="signShow = true"></u-button>
</view>
</view>
<view slot="value" class="mt-10">
<sign v-if="signShow" :signShow.sync="signShow" @confirm="signConfirm"></sign>
</view>
</u-cell>
<u-cell v-if="form.INSPECTION_USER_SIGN_IMG">
<view slot="value" class="mt-10 flex-1" v-show="form.INSPECTION_USER_SIGN_IMG">
<u--image v-show="form.INSPECTION_USER_SIGN_IMG" :showLoading="true" :src="form.INSPECTION_USER_SIGN_IMG"
width="100%" height="80px" mode="scaleToFill"
@click="previewImage(form.INSPECTION_USER_SIGN_IMG)"></u--image>
<sign v-if="signShow" :signShow.sync="signShow" @confirm="signConfirm"></sign>
</view>
</u-cell>
</template>
@ -957,5 +964,8 @@ export default {
}
</script>
<style>
<style scoped>
.flex-1 {
flex: 1;
}
</style>

View File

@ -38,13 +38,20 @@
<u-cell class="flex-none">
<view slot="title" class="title flex-between">
确认人签字
<u-button type="primary" text="手写添加" size="mini" class="bth-mini" @click="signShow = true"></u-button>
<view>
<u-button type="primary" text="手写添加" size="mini" class="bth-mini" @click="signShow = true"></u-button>
<sign v-if="signShow" :signShow.sync="signShow" @confirm="signConfirm"></sign>
</view>
</view>
<view slot="value" class="mt-10">
</view>
</u-cell>
<u-cell v-if="form.SIGN_IMG">
<view slot="value" class="mt-10 flex-1" v-show="form.SIGN_IMG">
<u--image v-show="form.SIGN_IMG" :showLoading="true" :src="form.SIGN_IMG"
width="100%" height="80px" mode="scaleToFill"
@click="previewImage(form.SIGN_IMG)"></u--image>
<sign v-if="signShow" :signShow.sync="signShow" @confirm="signConfirm"></sign>
</view>
</u-cell>
</template>
@ -58,15 +65,19 @@
<u-cell class="flex-none">
<view slot="title" class="title flex-between">
确认人签字
<u-button type="primary" text="手写添加" size="mini" class="bth-mini" @click="signShow = true"></u-button>
<view>
<u-button type="primary" text="手写添加" size="mini" class="bth-mini" @click="signShow = true"></u-button>
<sign v-if="signShow" :signShow.sync="signShow" @confirm="signConfirm"></sign>
</view>
</view>
<view slot="value" class="mt-10">
<u--image v-show="form.SIGN_IMG" :showLoading="true" :src="form.SIGN_IMG"
width="100%" height="80px" mode="scaleToFill"
@click="previewImage(form.SIGN_IMG)"></u--image>
<sign v-if="signShow" :signShow.sync="signShow" @confirm="signConfirm"></sign>
</view>
</u-cell>
<view>
<u--image v-show="form.SIGN_IMG" :showLoading="true" :src="form.SIGN_IMG"
width="100%" height="80px" mode="scaleToFill"
@click="previewImage(form.SIGN_IMG)"></u--image>
</view>
</template>
<u-cell class="flex-none" :border="false">
<view slot="value" class="flex-between">
@ -174,5 +185,10 @@ export default {
</script>
<style scoped>
.title {
min-width: 150rpx;
}
.flex-1 {
flex: 1;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -1,31 +0,0 @@
## 1.0.42023-04-17
- chore: 删除多余字符
## 1.0.32023-04-15
- BUG: QQ小程序可以使用会有报错但不影响使用
## 1.0.22023-04-15
- feat: 支持横屏
- BUG: QQ小程序无法使用为UNI官方问题插件所需要的API传this都会报错。
## 1.0.12023-04-03
- fix: 销毁时报错
## 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
- 首次上传
- 撤消、清空、保存、模拟压感等功能

View File

@ -1,64 +0,0 @@
export const uniContext = (ctx) => {
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 function useCurrentPage() {
const pages = getCurrentPages();
return pages[pages.length - 1];
}
export const toDataURL = (canvasId, context, options = {}) => {
// #ifdef MP-QQ
// context = context.$scope
// #endif
// #ifdef MP-ALIPAY
context = ''
// #endif
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
...options,
canvasId,
success: (res) => {
resolve(res.tempFilePath)
},
fail: (err) => {
reject(err)
}
}, context)
})
}

View File

@ -1,383 +0,0 @@
<template>
<view class="lime-signature" v-if="show" :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"
:width="canvasWidth"
:height="canvasHeight"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
@mousedown="touchStart"
@mousemove="touchMove"
@mouseup="touchEnd"
></canvas>
<canvas
class="offscreen"
canvas-id="offscreen"
id="offscreen"
:style="'width:' + offscreenSize[0] + 'px;height:' + offscreenSize[1] + 'px'"
:width="offscreenSize[0]"
:height="offscreenSize[1]">
</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">
export {default} from './render'
</script>
<!-- #endif -->
<script>
// #ifndef APP-NVUE
import {canIUseCanvas2d, wrapEvent, requestAnimationFrame, sleep} from './utils'
import {Signature} from './signature'
// import {Signature} from '@signature';
import {uniContext, createImage, toDataURL} from './context'
// #endif
import props from './props';
import {base64ToPath, getRect} from './utils'
export default {
props,
data() {
return {
canvasWidth: null,
canvasHeight: null,
useCanvas2d: true,
show: true,
offscreenStyles: '',
// #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}`
},
offscreenId() {
return this.canvasId + 'offscreen'
},
offscreenSize() {
const {canvasWidth, canvasHeight} = this
return this.landscape ? [canvasHeight, canvasWidth] : [canvasWidth, canvasHeight]
},
canvasStyle() {
const {canvasWidth, canvasHeight, backgroundColor} = this
return {
width: canvasWidth && (canvasWidth + 'px'),
height: canvasHeight && (canvasHeight + 'px'),
background: backgroundColor
}
},
param() {
const {penColor, penSize, backgroundColor, landscape, openSmooth, minLineWidth, maxLineWidth, minSpeed, maxWidthDiffRate, maxHistoryLength, disableScroll} = this
return JSON.parse(JSON.stringify({penColor, penSize, backgroundColor, landscape, 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' && canIUseCanvas2d()
},
// #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.stopWatch()
this.signature.destroy()
this.signature = null
this.show = false;
},
// #endif
// #ifdef VUE2
beforeDestroy() {
this.stopWatch && this.stopWatch()
this.signature.destroy()
this.show = false;
this.signature = null
},
// #endif
// #endif
methods: {
// #ifdef MP-QQ
// toJSON() { return this },
// #endif
// #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 = []
} 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 {backgroundColor, landscape} = this
const width = this.signature.canvas.get('width')
const height = this.signature.canvas.get('height')
const canvasToTempFilePath = (image) => {
const context = uni.createCanvasContext('offscreen', this)
context.save()
context.setTransform(1,0,0,1,0,0)
if(landscape) {
context.translate(0, width)
context.rotate(-Math.PI / 2)
}
if(backgroundColor) {
context.fillStyle = backgroundColor
context.fillRect(0,0, width, height)
}
context.drawImage(image, 0, 0, width, height);
context.draw(false, () => {
toDataURL('offscreen', this, param).then((res) => {
const size = Math.max(width, height)
context.restore()
context.clearRect(0,0, size, size)
success({tempFilePath: res, isEmpty})
})
})
}
if(this.useCanvas2d) {
try{
// #ifndef MP-ALIPAY
base64ToPath(canvas.toDataURL()).then(canvasToTempFilePath)
// #endif
// #ifdef MP-ALIPAY
canvas.toTempFilePath({
canvasid: this.canvasid,
success(res){
canvasToTempFilePath(res.tempFilePath)
},
fail
})
// #endif
} catch(err){fail(err)}
} else {
toDataURL(this.canvasId, this).then(canvasToTempFilePath).catch(fail)
}
// #endif
},
// #ifndef APP-PLUS
getContext() {
return getRect(`#${this.canvasId}`, {context: this, type: this.useCanvas2d ? 'fields': 'boundingClientRect'}).then(res => {
if(res) {
let {width, height, node: canvas, left, top, right} = res
let {pixelRatio} = uni.getSystemInfoSync()
let context;
if(canvas) {
context = canvas.getContext('2d')
canvas.width = width * pixelRatio;
canvas.height = height * pixelRatio;
} else {
pixelRatio = 1
context = uniContext(uni.createCanvasContext(this.canvasId, this))
canvas = {
createImage,
toDataURL: () => toDataURL(this.canvasId, this),
requestAnimationFrame
}
}
// 使stroke
context.clearRect(0,0,width,height)
return { left, top, right, width, height, context, canvas, pixelRatio};
}
})
},
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
.offscreen {
position: fixed;
top: 0
left: 1500rpx;
}
</style>

View File

@ -1,56 +0,0 @@
export default {
styles: String,
disableScroll: {
type: Boolean,
default: true
},
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
},
landscape: {
type: Boolean
}
}

View File

@ -1,112 +0,0 @@
// #ifdef APP-VUE
// import { Signature } from '@signature'
import { Signature } from './signature'
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,landscape} = this.options
if (landscape || 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')
const size = [width, height]
if(landscape) {size.reverse()}
canvas.width = size[0] * pixelRatio
canvas.height = size[1] * pixelRatio
const context = canvas.getContext('2d')
context.scale(pixelRatio, pixelRatio)
if (landscape) {
context.translate(0, width)
context.rotate(-Math.PI / 2)
}
if (backgroundColor) {
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
})
}
}
},
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

File diff suppressed because one or more lines are too long

View File

@ -1,139 +0,0 @@
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
}
function gte(version) {
let {SDKVersion} = uni.getSystemInfoSync();
// #ifdef MP-ALIPAY
SDKVersion = my.SDKVersion
// #endif
return compareVersion(SDKVersion, version) >= 0;
}
export function canIUseCanvas2d() {
// #ifdef MP-WEIXIN
return gte('2.9.0');
// #endif
// #ifdef MP-ALIPAY
return gte('2.7.0');
// #endif
// #ifdef MP-TOUTIAO
return gte('1.78.0');
// #endif
return false
}
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) => {
// #ifdef MP
const fs = uni.getFileSystemManager()
//自定义文件名
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
const filePath = `${uni.env.USER_DATA_PATH}/${time}.${format}`;
fs.writeFile({
filePath,
data: base64.split(',')[1],
encoding: 'base64',
success() {
resolve(filePath)
},
fail(err) {
reject(err)
}
})
// #endif
// #ifdef APP-PLUS
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)
})
// #endif
})
}
export function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay))
}
export function getRect(selector, options = {}) {
const typeDefault = 'boundingClientRect'
const { context, type = typeDefault} = options
return new Promise((resolve, reject) => {
const dom = uni.createSelectorQuery().in(context).select(selector);
const result = (rect) => {
if(rect) {
resolve(rect)
} else {
reject()
}
}
if(type == typeDefault) {
dom[type](result).exec()
} else {
dom[type]({
node: true,
size: true,
rect: true
}, result).exec()
}
});
};

View File

@ -1,63 +0,0 @@
<template>
<view>
<view style="width: 750rpx; height: 950rpx;">
<l-signature disableScroll backgroundColor="#000" ref="signatureRef" :penColor="penColor" :penSize="penSize"
:minLineWidth="2" :openSmooth="openSmooth"></l-signature>
</view>
<!-- <uni-popup ref="popup" type="bottom">
<l-signature v-if="show" beforeDelay="1000" disableScroll backgroundColor="#000" ref="signatureRef2" :penColor="penColor"
:penSize="penSize" :minLineWidth="2" :openSmooth="openSmooth"></l-signature>
</uni-popup> -->
<view>
<!-- <button @click="open"></button> -->
<button @click="onClick('clear')"></button>
<button @click="onClick('undo')"></button>
<button @click="onClick('save')"></button>
<button @click="onClick('openSmooth')">{{openSmooth?'':''}}</button>
</view>
<image :src="url" v-if="url" mode="widthFix"></image>
</view>
</template>
<script>
export default {
data() {
return {
title: 'Hello',
penColor: 'red',
penSize: 16,
url: '',
show: false,
openSmooth: true,
bottomHeight: 0,
customBar: this.CustomBar || 0,
}
},
methods: {
open() {
// refuni-popup , type ['top','left','bottom','right','center']
this.$refs.popup.open()
this.show = true
},
onClick(type) {
if (type == 'openSmooth') {
this.openSmooth = !this.openSmooth
return
}
if (type == 'save') {
this.$refs.signatureRef.canvasToTempFilePath({
success: (res) => {
this.url = res.tempFilePath
}
})
return
}
if (this.$refs.signatureRef)
this.$refs.signatureRef[type]()
}
}
}
</script>
<style>
</style>

View File

@ -1,77 +0,0 @@
{
"id": "lime-signature",
"displayName": "手写板-签名签字",
"version": "1.0.4",
"description": "手写板签名插件一款能跑在uniapp各端中的签名插件支持横屏、背景色、笔画颜色、笔画大小等功能",
"keywords": [
"写字",
"签名",
"电子签名",
"签字",
"横屏"
],
"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": "n"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -1,105 +0,0 @@
# 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)
// 生成图片的临时路径
// H5 生成的是base64
this.url = res.tempFilePath
}
})
return
}
if (this.$refs.signatureRef)
this.$refs.signatureRef[type]()
}
}
}
```
### 插件标签
- 默认 l-signature 为 component
- 默认 lime-signature 为 demo
## 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` |
| landscape | 横屏 | <em>boolean</em> | `` |
| disableScroll | 当在写字时,禁止屏幕滚动以及下拉刷新 | <em>boolean</em> | `true` |
### 事件 Events
| 事件名 | 说明 | 回调 |
| ------- | ------------ | -------------- |
| undo | 撤消,回退到上一步 | |
| clear | 清空,清空画板 | |
| canvasToTempFilePath | 保存生成图片与官方保持一致但不需要传canvasId | |
### 常见问题
- 放在弹窗里时,尺寸不对 可以延时手写板出现时机给手写板加vif或beforeDelay="300"
### 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

View File

@ -1,131 +0,0 @@
<!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()
const {backgroundColor, landscape} = options
if(backgroundColor || landscape) {
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')
const size = [width, height]
if(landscape) {size.reverse()}
canvas.width = size[0] * pixelRatio
canvas.height = size[1] * pixelRatio
const context = canvas.getContext('2d')
context.scale(pixelRatio, pixelRatio)
if (landscape) {
context.translate(0, width)
context.rotate(-Math.PI / 2)
}
if (backgroundColor) {
context.fillStyle = backgroundColor
context.fillRect(0, 0, width, height)
}
context.drawImage(this.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

View File

@ -0,0 +1,50 @@
## 1.1.82024-08-21
1. 修复h5端横屏时可能会出现签名回显的问题
## 1.1.72024-07-23
1. 修复签字偏移问题
## 1.1.62024-06-28
1.新增needBack属性点击取消时是否需要自动返回默认true
2.修正原取消事件cancel单词写错成cancle的问题
## 1.1.52024-06-28
1.优化
## 1.1.42024-05-10
1. 删除冗余代码
2. 文档迁移
## 1.1.32024-04-29
1. 更新示例工程
## 1.1.22024-04-29
1. 优化了多场景下签名的需求,详情请看示例工程
## 1.1.12024-04-29
1. 更新
## 1.1.02024-04-01
1. 更好的兼容支付宝小程序
## 1.0.92024-03-20
1. 修复小程序无法横屏的问题
## 1.0.82024-02-28
1. 修复背景颜色修改无效的问题
## 1.0.72024-02-04
1. 修复签名板在h5端高度异常问题
2. 更新示例工程
3. 更新文档
## 1.0.62023-10-20
1. 新增@firstTouchStart首次触碰绘制的方法
2. 修复添加动态水印时初次加载水印失效的bug
3. 更新示例项目中水印的使用方式
## 1.0.52023-10-17
1. 更新示例项目
## 1.0.42023-10-17
1. 更新文档说明
## 1.0.32023-10-17
1. 新增图片导出格式与画质配置
2. 更新条件编译以更好适配H5、App和小程序端
## 1.0.22023-10-16
1. 修复横屏适配问题
2. 文档更新
## 1.0.12023-10-16
1. 修复安卓真机canvas无法动态设置宽高的bug
2. 修复安卓真机水印与背景底色失效的bug
3. 新增监听path字段现在不仅可以返回base64图片也可返回临时的图片路径
4. 修复vue2、vue3兼容性
## 1.0.02023-10-13
1. 签字板可添加水印,背景图片,调整画笔样式等
2. 适配横竖屏生成base64格式图片

View File

@ -0,0 +1,196 @@
function getLocalFilePath(path) {
if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path.indexOf('_downloads') === 0) {
return path
}
if (path.indexOf('file://') === 0) {
return path
}
if (path.indexOf('/storage/emulated/0/') === 0) {
return path
}
if (path.indexOf('/') === 0) {
var localFilePath = plus.io.convertAbsoluteFileSystem(path)
if (localFilePath !== path) {
return localFilePath
} else {
path = path.substr(1)
}
}
return '_www/' + path
}
function dataUrlToBase64(str) {
var array = str.split(',')
return array[array.length - 1]
}
var index = 0
function getNewFileId() {
return Date.now() + String(index++)
}
function biggerThan(v1, v2) {
var v1Array = v1.split('.')
var v2Array = v2.split('.')
var update = false
for (var index = 0; index < v2Array.length; index++) {
var diff = v1Array[index] - v2Array[index]
if (diff !== 0) {
update = diff > 0
break
}
}
return update
}
export function pathToBase64(path) {
return new Promise(function(resolve, reject) {
if (typeof window === 'object' && 'document' in window) {
if (typeof FileReader === 'function') {
var xhr = new XMLHttpRequest()
xhr.open('GET', path, true)
xhr.responseType = 'blob'
xhr.onload = function() {
if (this.status === 200) {
let fileReader = new FileReader()
fileReader.onload = function(e) {
resolve(e.target.result)
}
fileReader.onerror = reject
fileReader.readAsDataURL(this.response)
}
}
xhr.onerror = reject
xhr.send()
return
}
var canvas = document.createElement('canvas')
var c2x = canvas.getContext('2d')
var img = new Image
img.onload = function() {
canvas.width = img.width
canvas.height = img.height
c2x.drawImage(img, 0, 0)
resolve(canvas.toDataURL())
canvas.height = canvas.width = 0
}
img.onerror = reject
img.src = path
return
}
if (typeof plus === 'object') {
plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), function(entry) {
entry.file(function(file) {
var fileReader = new plus.io.FileReader()
fileReader.onload = function(data) {
resolve(data.target.result)
}
fileReader.onerror = function(error) {
reject(error)
}
fileReader.readAsDataURL(file)
}, function(error) {
reject(error)
})
}, function(error) {
reject(error)
})
return
}
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
wx.getFileSystemManager().readFile({
filePath: path,
encoding: 'base64',
success: function(res) {
resolve('data:image/png;base64,' + res.data)
},
fail: function(error) {
reject(error)
}
})
return
}
reject(new Error('not support'))
})
}
export function base64ToPath(base64) {
return new Promise(function(resolve, reject) {
if (typeof window === 'object' && 'document' in window) {
base64 = base64.split(',')
var type = base64[0].match(/:(.*?);/)[1]
var str = atob(base64[1])
var n = str.length
var array = new Uint8Array(n)
while (n--) {
array[n] = str.charCodeAt(n)
}
return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], { type: type })))
}
var extName = base64.split(',')[0].match(/data\:\S+\/(\S+);/)
if (extName) {
extName = extName[1]
} else {
reject(new Error('base64 error'))
}
var fileName = getNewFileId() + '.' + extName
if (typeof plus === 'object') {
var basePath = '_doc'
var dirPath = 'uniapp_temp'
var filePath = basePath + '/' + dirPath + '/' + fileName
if (!biggerThan(plus.os.name === 'Android' ? '1.9.9.80627' : '1.9.9.80472', plus.runtime.innerVersion)) {
plus.io.resolveLocalFileSystemURL(basePath, function(entry) {
entry.getDirectory(dirPath, {
create: true,
exclusive: false,
}, function(entry) {
entry.getFile(fileName, {
create: true,
exclusive: false,
}, function(entry) {
entry.createWriter(function(writer) {
writer.onwrite = function() {
resolve(filePath)
}
writer.onerror = reject
writer.seek(0)
writer.writeAsBinary(dataUrlToBase64(base64))
}, reject)
}, reject)
}, reject)
}, reject)
return
}
var bitmap = new plus.nativeObj.Bitmap(fileName)
bitmap.loadBase64Data(base64, function() {
bitmap.save(filePath, {}, function() {
bitmap.clear()
resolve(filePath)
}, function(error) {
bitmap.clear()
reject(error)
})
}, function(error) {
bitmap.clear()
reject(error)
})
return
}
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
var filePath = wx.env.USER_DATA_PATH + '/' + fileName
wx.getFileSystemManager().writeFile({
filePath: filePath,
data: dataUrlToBase64(base64),
encoding: 'base64',
success: function() {
resolve(filePath)
},
fail: function(error) {
reject(error)
}
})
return
}
reject(new Error('not support'))
})
}

View File

@ -0,0 +1,459 @@
<template>
<view class="sign-page">
<view class="sign-body">
<canvas
id="signCanvas"
canvas-id="signCanvas"
class="sign-canvas"
disable-scroll
@touchstart="signCanvasStart"
@touchmove="signCanvasMove"
@touchend="signCanvasEnd"
></canvas>
<!-- #ifndef APP -->
<!--用于临时储存横屏图片的canvas容器H5和小程序需要-->
<canvas
v-if="horizontal"
id="hsignCanvas"
canvas-id="hsignCanvas"
style="position: absolute; left: -1000px; z-index: -1"
:style="{ width: canvasHeight + 'px', height: canvasWidth + 'px' }"
></canvas>
<!-- #endif -->
</view>
<!-- <view class="sign-footer" :class="[horizontal ? 'horizontal-btns' : 'vertical-btns']">-->
<!-- <view class="btn" @click="cancel"></view>-->
<!-- <view class="btn" @click="reset"></view>-->
<!-- <view class="btn" @click="confirm"></view>-->
<!-- </view>-->
</view>
</template>
<script>
import { pathToBase64, base64ToPath } from './index.js'
export default {
name: 'sign',
props: {
// id
sid: {
type: String,
default: 'sign-board'
},
// bgColor
bgImg: {
type: String,
default: ''
},
//
bgColor: {
type: String,
default: ''
},
//
showMark: {
type: Boolean,
default: true
},
//
markText: {
type: Array,
default: () => {
return [] // ['1', '2']
}
},
//
markStyle: {
type: Object,
default: () => {
return {
fontSize: 12, //
fontFamily: 'microsoft yahei', //
color: '#cccccc', //
rotate: 60, //
step: 2.2 // 1.4-2.6
}
}
},
//
horizontal: {
type: Boolean,
default: false
},
//
penStyle: {
type: Object,
default: () => {
return {
lineWidth: 3, // 线 1~5
color: '#000000' //
}
}
},
//
expFile: {
type: Object,
default: () => {
return {
fileType: 'png', // png/jpg (pngjpg)
quality: 1 // 0 - 1 (jpg)
}
}
},
//
needBack: {
type: Boolean,
default: true
}
},
data() {
return {
canvasCtx: null, // canvascanvasWidth: 0, // canvas
canvasWidth: 0, // canvas
canvasHeight: 0, // canvas
x0: 0, // touchmove
y0: 0, // touchmove
signFlag: false //
}
},
computed: {},
created() {},
mounted() {
this.$nextTick(() => {
this.createCanvas()
})
},
methods: {
// canvas
createCanvas() {
this.canvasCtx = uni.createCanvasContext('signCanvas', this)
this.canvasCtx.setLineCap('round') // 线线
// canvas
const query = uni.createSelectorQuery().in(this)
query
.select('.sign-body')
.boundingClientRect((data) => {
this.canvasWidth = data.width
this.canvasHeight = data.height
})
.exec(async () => {
await this.drawBg()
this.drawMark(this.markText)
})
},
async drawBg() {
if (this.bgImg) {
const img = await uni.getImageInfo({ src: this.bgImg })
this.canvasCtx.drawImage(img.path, 0, 0, this.canvasWidth, this.canvasHeight)
} else if (this.bgColor) {
//
this.canvasCtx.setFillStyle(this.bgColor)
this.canvasCtx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
}
},
//
drawMark(textArray) {
if (!this.showMark) {
this.canvasCtx.draw()
return
}
//
this.drawBg()
//
const markStyle = Object.assign(
{
fontSize: 12, //
fontFamily: 'microsoft yahei', //
color: '#cccccc', //
rotate: 60, //
step: 2 // 1.4-2.6
},
this.markStyle
)
this.canvasCtx.font = `${markStyle.fontSize}px ${markStyle.fontFamily}`
this.canvasCtx.fillStyle = markStyle.color
//
const maxPx = Math.max(this.canvasWidth / 2, this.canvasHeight / 2)
const stepPx = Math.floor(maxPx / markStyle.step)
let arrayX = [0] // canvas 0 0
while (arrayX[arrayX.length - 1] < maxPx / 2) {
arrayX.push(arrayX[arrayX.length - 1] + stepPx)
}
arrayX.push(
...arrayX.slice(1, arrayX.length).map((item) => {
return -item
})
)
for (let i = 0; i < arrayX.length; i++) {
for (let j = 0; j < arrayX.length; j++) {
this.canvasCtx.save()
this.canvasCtx.translate(this.canvasWidth / 2, this.canvasHeight / 2) //
this.canvasCtx.rotate(Math.PI * (markStyle.rotate / 180))
textArray.forEach((item, index) => {
let offsetY = markStyle.fontSize * index
this.canvasCtx.fillText(item, arrayX[i], arrayX[j] + offsetY)
})
this.canvasCtx.restore()
}
}
this.canvasCtx.draw()
},
cancel() {
//
this.$emit('cancel')
this.reset()
if (this.needBack) uni.navigateBack()
},
async reset() {
this.$emit('reset')
this.signFlag = false
this.canvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
await this.drawBg()
this.drawMark(this.markText)
},
async confirm() {
this.$emit('confirm')
//
if (!this.signFlag) {
uni.showToast({
title: '请签名后再点击确定',
icon: 'none',
duration: 2000
})
return
}
// uni.showModal({
// title: '',
// content: '',
// showCancel: true,
// success: async ({ confirm }) => {
// if (confirm) {
let tempFile
if (this.horizontal) {
tempFile = await this.saveHorizontalCanvas()
} else {
tempFile = await this.saveCanvas()
}
const base64 = await pathToBase64(tempFile)
const path = await base64ToPath(base64)
uni.$emit('getSignImg', { base64, path, sid: this.sid })
if (this.needBack) uni.navigateBack()
// }
// }
// })
},
signCanvasEnd(e) {
//
// console.log(e, 'signCanvasEnd')
this.x0 = 0
this.y0 = 0
},
signCanvasMove(e) {
//
// console.log(e, 'signCanvasMove')
let dx = e.touches[0].x
let dy = e.touches[0].y
this.canvasCtx.moveTo(this.x0, this.y0)
this.canvasCtx.lineTo(dx, dy)
this.canvasCtx.setLineWidth(this.penStyle?.lineWidth || 4)
this.canvasCtx.strokeStyle = this.penStyle?.color || '#000000' //
this.canvasCtx.stroke()
this.canvasCtx.draw(true)
this.x0 = e.touches[0].x
this.y0 = e.touches[0].y
},
signCanvasStart(e) {
// appeapp
// console.log('signCanvasStart', e)
if (!this.signFlag) {
//
this.$emit('firstTouchStart')
}
this.signFlag = true
this.x0 = e.touches[0].x
this.y0 = e.touches[0].y
},
//
async saveCanvas() {
return await new Promise((resolve, reject) => {
uni.canvasToTempFilePath(
{
canvasId: 'signCanvas',
fileType: this.expFile.fileType, // pngjpg
quality: this.expFile.quality, // 0 - 1
success: (res) => {
if (!res.tempFilePath) {
uni.showModal({
title: '提示',
content: '保存签名失败',
showCancel: false
})
return
}
resolve(res.tempFilePath)
},
fail: (r) => {
console.log('图片生成失败:' , r)
resolve(false)
}
},
this
)
})
},
//
async saveHorizontalCanvas() {
return await new Promise((resolve, reject) => {
uni.canvasToTempFilePath(
{
canvasId: 'signCanvas',
fileType: this.expFile.fileType, // pngjpg
success: (res) => {
if (!res.tempFilePath) {
uni.showModal({
title: '提示',
content: '保存签名失败',
showCancel: false
})
return
}
// #ifdef APP
uni.compressImage({
src: res.tempFilePath,
quality: this.expFile.quality * 100, // 0 - 100
rotate: 270,
success: (r) => {
console.log('==== compressImage :', r)
resolve(r.tempFilePath)
}
})
// #endif
// #ifndef APP
uni.getImageInfo({
src: res.tempFilePath,
success: (r) => {
// console.log('==== getImageInfo :', r)
// signCanvashsignCanvas
const hcanvasCtx = uni.createCanvasContext('hsignCanvas', this)
//
hcanvasCtx.translate(this.canvasHeight / 2, this.canvasWidth / 2)
hcanvasCtx.rotate(Math.PI * (-90 / 180))
hcanvasCtx.drawImage(
r.path,
-this.canvasWidth / 2,
-this.canvasHeight / 2,
this.canvasWidth,
this.canvasHeight
)
hcanvasCtx.draw(false, async () => {
const hpathRes = await uni.canvasToTempFilePath(
{
canvasId: 'hsignCanvas',
fileType: this.expFile.fileType, // pngjpg
quality: this.expFile.quality // 0 - 1
},
this
)
let tempFile = ''
if (Array.isArray(hpathRes)) {
hpathRes.some((item) => {
if (item) {
tempFile = item.tempFilePath
return
}
})
} else {
tempFile = hpathRes.tempFilePath
}
resolve(tempFile)
})
}
})
// #endif
},
fail: (err) => {
console.log('图片生成失败:' , err)
resolve(false)
}
},
this
)
})
}
}
}
</script>
<style scoped lang="scss">
.sign-page {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
.sign-body {
width: 100%;
flex-grow: 1;
.sign-canvas {
width: 100%;
height: 100%;
}
}
.sign-footer {
width: 100%;
height: 88rpx;
display: flex;
justify-content: space-evenly;
align-items: center;
border-top: 1px solid #cccccc;
box-sizing: border-box;
.btn {
line-height: 66rpx;
text-align: center;
border-radius: 12rpx;
&:nth-child(1) {
background-color: #ff0800;
color: #ffffff;
}
&:nth-child(2) {
background-color: #00d000;
color: #ffffff;
}
&:nth-child(3) {
background-color: #0184ff;
color: #ffffff;
}
}
}
.vertical-btns {
.btn {
width: 120rpx;
height: 66rpx;
}
}
.horizontal-btns {
.btn {
width: 66rpx;
height: 120rpx;
writing-mode: vertical-lr;
transform: rotate(90deg);
}
}
}
</style>

View File

@ -0,0 +1,85 @@
{
"id": "sp-sign-board",
"displayName": "签字板 签名版 手写 动态水印 可叠加背景 可导出图片",
"version": "1.1.8",
"description": "签字板可添加背景图或动态水印可适配横竖屏并将签名生成base64格式图片",
"keywords": [
"签字",
"签名",
"水印",
"base64"
],
"repository": "",
"engines": {
"HBuilderX": "^3.5.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,11 @@
# sp-sign-board
### 文档迁移
> 防止文档失效,提供下列五个地址,内容一致
- [地址一](https://sonvee.github.io/sv-app-docs/docs-github/src/plugins/sp-sign-board/sp-sign-board.html)
- [地址二](https://sv-app-docs.pages.dev/src/plugins/sp-sign-board/sp-sign-board.html)
- [地址三](https://sv-app-docs.4everland.app/src/plugins/sp-sign-board/sp-sign-board.html)
- [地址四](https://sv-app-docs.vercel.app/src/plugins/sp-sign-board/sp-sign-board.html) (需要梯子)
- [地址五](https://static-mp-74bfcbac-6ba6-4f39-8513-8831390ff75a.next.bspapp.com/docs-uni/src/plugins/sp-sign-board/sp-sign-board.html) (有IP限制)

View File

@ -28,9 +28,11 @@ const updateVersion = {
_this.platform = res.platform
}
});
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, function (inf) {
_this.version = inf.version; //获取当前版本号
});
// #endif
console.info('2')
await _this.getUrlVarsion(toast)
},
@ -77,6 +79,27 @@ const updateVersion = {
}
// #endif
// #ifdef MP-WEIXIN
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function (res) {
// 请求完新版本信息的回调
console.log(res.hasUpdate,'版本信息');
if (res.hasUpdate) {
updateManager.onUpdateReady(function (res) {
uni.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
}
});
});
}
});
// #endif
},
modalConfirm() {
this.updateVersion.confirmType === 'download' ? this.downloadNewVersion() : this.installNewVersion()