This commit is contained in:
曹辉 2023-04-12 15:39:24 +08:00
parent 58bcab47fa
commit edf79911ef
20 changed files with 954 additions and 284 deletions

View File

@ -1,245 +0,0 @@
<template>
<view class="signature-wrap">
<canvas :canvas-id="cid" :id="cid" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"
style="height:600rpx" :style="[{ width: formatSize(width)}, customStyle]"></canvas>
<slot />
</view>
</template>
<script>
/**
* sign canvas 手写签名
* @description 设置线条宽度颜色撤回清空
* @tutorial
* @property {String} cid canvas id 不设置则默认为 v-sign-时间戳
* @property {String, Number} width canvas 宽度
* @property {String, Number} height canvas 高度
* @property {Object} customStyle 自定义样式
* @property {String} lineColor 画笔颜色
* @property {Number} lineWidth 画笔大小权重大于 v-sign-pen 组件设置的画笔大小
* @event {Function} init 当创建完 canvas 实例后触发向外提供 canvas实例撤回清空方法
* @example <v-sign @init="signInit"></v-sign>
*/
import {
formatSize
} from './utils'
// convas
let canvasCtx
export default {
name: 'v-sign',
props: {
// canvas id
cid: {
type: String,
default: `v-sign-${Date.now()}`
// required: true
},
// canvas
width: {
type: [String, Number],
default: '100%'
},
// canvas
height: {
type: [String, Number],
default: 300
},
// v-sign-pen
lineWidth: {
type: Number
},
// 线
lineColor: {
type: String,
default: '#000'
},
// canvas
customStyle: {
type: Object,
default: () => ({})
}
},
provide() {
return {
getInterface: this.provideInterface
}
},
data() {
return {
formatSize,
lineData: [],
winWidth: 0,
winHeight: 0,
penLineWidth: null, // v-sign-pen
}
},
mounted() {
canvasCtx = uni.createCanvasContext(this.cid, this)
//
this.$emit('init', this.provideInterface())
//
uni.getSystemInfo({
success: res => {
this.winWidth = res.windowWidth
this.winHeight = res.windowHeight
}
})
},
methods: {
onTouchStart(e) {
const pos = e.touches[0]
this.lineData.push({
style: {
color: this.lineColor,
width: this.lineWidth || this.penLineWidth || 4
},
//
coordinates: [{
type: e.type,
x: pos.x,
y: pos.y
}]
})
this.drawLine()
},
onTouchMove(e) {
const pos = e.touches[0]
this.lineData[this.lineData.length - 1].coordinates.push({
type: e.type,
x: pos.x,
y: pos.y
})
this.drawLine()
},
onTouchEnd(e) {
// console.log(e.type, e)
},
//
clear() {
this.lineData = []
canvasCtx.clearRect(0, 0, this.winWidth, this.winHeight)
canvasCtx.draw()
},
//
revoke() {
this.lineData.pop()
this.lineData.forEach((item, index) => {
canvasCtx.beginPath()
canvasCtx.setLineCap('round')
canvasCtx.setStrokeStyle(item.style.color)
canvasCtx.setLineWidth(item.style.width)
item.coordinates.forEach(pos => {
if (pos.type == 'touchstart') {
canvasCtx.moveTo(pos.x, pos.y)
} else {
canvasCtx.lineTo(pos.x, pos.y)
}
})
canvasCtx.stroke()
})
canvasCtx.draw()
},
// 线
drawLine() {
const lineDataLen = this.lineData.length
if (!lineDataLen) return
const currentLineData = this.lineData[lineDataLen - 1]
const coordinates = currentLineData.coordinates
const coordinatesLen = coordinates.length
if (!coordinatesLen) return
let startPos
let endPos
if (coordinatesLen < 2) {
// only start, no move event
startPos = coordinates[coordinatesLen - 1]
endPos = {
x: startPos.x + 1,
y: startPos.y
}
} else {
startPos = coordinates[coordinatesLen - 2]
endPos = coordinates[coordinatesLen - 1]
}
const style = currentLineData.style
canvasCtx.beginPath()
canvasCtx.setLineCap('round')
canvasCtx.setStrokeStyle(style.color)
canvasCtx.setLineWidth(style.width)
canvasCtx.moveTo(startPos.x, startPos.y)
canvasCtx.lineTo(endPos.x, endPos.y)
// const P1 = this.caculateBezier(startPos, endPos, centerPos)
// console.log(P1.x, P1.y)
// canvasCtx.moveTo(startPos.x, startPos.y)
// canvasCtx.quadraticCurveTo(P1.x, P1.y, endPos.x, endPos.y)
canvasCtx.stroke()
canvasCtx.draw(true)
},
canvasToTempFilePath(conf = {}) {
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvasId: this.cid,
...conf,
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
console.log('fail', err)
reject(err)
}
},
this
)
})
},
setLineWidth(numberVal) {
this.penLineWidth = numberVal
},
provideInterface() {
return {
cid: this.cid,
ctx: canvasCtx,
clear: this.clear,
revoke: this.revoke,
canvasToTempFilePath: this.canvasToTempFilePath,
setLineWidth: this.setLineWidth
}
},
/**
* 计算二次贝塞尔曲线 控制点 P1
* 起点 P0(x0,y0)控制点P1(x1, y1)P2(x2, y2)曲线上任意点B(x, y)
* 二次贝塞尔公式B(t) = (1-t)²P0 + 2t(1-t)P1 + t²P2
* 代入坐标得
* x = (1-t)²*x0 + 2t(1-t)*x1 + *x2
* y = (1-t)²*y0 + 2t(1-t)*y1 + *y2
*/
caculateBezier(P0, P2, B, t = 0.5) {
const {
x: x0,
y: y0
} = P0
const {
x: x2,
y: y2
} = P2
const {
x,
y
} = B
let x1 = (x - (1 - t) * (1 - t) * x0 - t * t * x2) / (2 * t * (1 - t))
let y1 = (y - (1 - t) * (1 - t) * y0 - t * t * y2) / (2 * t * (1 - t))
return {
x: x1,
y: y1
}
}
}
}
</script>
<style lang="scss" scoped>
.signature-wrap {
position: relative;
}
</style>

View File

@ -9,7 +9,13 @@
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
},{
"path": "pages/confirmCompletion/confirmCompletion",
"style": {
"navigationBarTitleText": "当前工单",
"enablePullDownRefresh": false
}
}, {
"path": "pages/MyBenefits/MyBenefits",
"style": {
"navigationBarTitleText": "我的收益",
@ -48,13 +54,6 @@
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "pages/confirmCompletion/confirmCompletion",
"style": {
"navigationBarTitleText": "当前工单",
"enablePullDownRefresh": false,
"disableScroll": true
}
}, {
"path": "pages/taskReturn/taskReturn",
"style": {

View File

@ -101,15 +101,11 @@
</view>
</view>
</u-popup>
<!-- <u-popup v-model="show" mode='bottom' :closeable='true' :safe-area-inset-bottom='true' @touchmove.stop.prevent
height='900'>
<signature @userSignaturePictureUrl='userSignaturePicture' @click.native.stop
style='background-color: #F4F5F7;width: 100%;height: 800rpx;'>
</signature>
</u-popup> -->
<u-mask :show="show" @click="show = false" @touchmove.stop.prevent>
<signature @userSignaturePictureUrl='userSignaturePicture' @click.native.stop v-if='show'
style='background-color: #F4F5F7;position:absolute;bottom:0%;width: 100%;height: 800rpx;'></signature>
<u-mask :show="show" @click="show = false">
<view style="position:absolute;bottom:0;height:800rpx;width:100%;background-color: #fff;" v-if='show'>
<signature @userSignaturePictureUrl='userSignaturePicture' @click.native.stop
style='background-color: #F4F5F7;width: 100%;height: 800rpx;'></signature>
</view>
</u-mask>
<u-toast ref="uToast" />
</view>
@ -292,8 +288,10 @@
.app {
font-size: 36rpx;
padding: 0;
overflow: scroll;
height: 100vh;
::v-deep .u-mask {
height: 100vh !important;
}
.finishmask {
height: 300rpx;

View File

@ -1,6 +1,6 @@
<template>
<view class="" style="background-color: #F4F5F7;">
<Signature @init="onSignInit" style='background-color: #fff;'></Signature>
<view class="signature" style="background-color: #F4F5F7;height: 400rpx;">
<Signature @init="onSignInit" style='background-color: #fff;height: 700rpx;width: 100%;'></Signature>
<view class="btns">
<button @click="clear">清空</button>
<button @click="revoke">撤回</button>
@ -10,7 +10,7 @@
</template>
<script>
import Signature from '@/components/v-sign/v-sign.vue'
import Signature from '../v-sign/components/v-sign/v-sign.vue'
export default {
components: {
Signature
@ -37,6 +37,11 @@
</script>
<style lang='scss'>
.signature {
height: 800rpx;
position: relative;
}
.btns {
margin-top: 50rpx;
display: flex;

48
pages/v-sign/changelog.md Normal file
View File

@ -0,0 +1,48 @@
## 0.5.22022-06-10
- canvas 默认设置 disable-scroll 属性为true, 当在 canvas 中移动时且有绑定手势事件时,禁止屏幕滚动以及下拉刷新
## 0.5.12022-05-27
- 取消属性 width, height 默认值
## 0.5.02022-05-27
- 新增 v-sgin 组件获取坐标信息数组方法 `getLineData`
- 新增 v-sign 组件事件触发 @clear, @revoke, @end
- 修复撤销操作重设背景色导致线条消失问题
## 0.4.02022-05-15
- 新增属性 bgColor 设置画布背景色,修复导出图片无背景色问题
## 0.3.42022-03-30
修复 v-sign-action 子组件 svg 加载报错
## 0.3.32022-03-30
修复 v-sign-action 子组件 svg 加载报错
## 0.3.22022-01-12
- 修复多组件共存绘制问题
## 0.3.12021-12-14
- 【新增】保存 png 图片方法 saveImage;
- 【修改】 控件子组件保存按钮动作由保存为临时文件路径改为保存图片
## 0.3.02021-12-11
- 添加颜色选择器组件
## 0.2.02021-12-10
- 修改为 uni_modules 规范
## 0.1.7 (2021-12-09)
- 画笔组件优化
## 0.1.6 (2021-12-08)
- 画笔样式优化
## 0.1.5 (2021-12-08)
- 画笔子组件样式修改支持circle、line 两种样式
## 0.1.4 (2021-12-08)
- 添加画笔子组件
## 0.1.3 (2021-12-02)
- 添加按钮控件子组件
## 0.1.0 (2021-11-28)
- 支持线宽、颜色自定义,自定义画布样式
- 支持画布清空、撤回、保存图片
- 事件 init 暴露清空、撤回、保存图片方法

View File

@ -7,7 +7,7 @@
:style="[{ 'margin-right': formatSize(space) }]"
@click="onBtnClick(item)"
>
<image :class="['icon', 'icon-' + item.action]" :src="item.icon"></image>
<image v-if="item.icon" :class="['icon', 'icon-' + item.action]" :src="item.icon"></image>
<text class="text">{{ item.label }}</text>
</view>
</view>
@ -25,7 +25,7 @@
* @event {Function} 点击对应类型按钮触发对应事件 例如点击 clear 则触发 clear 事件
* @example 示例
**/
import { formatSize } from './utils'
import { formatSize } from '../../utils'
// v-sign
let vSignInterface
@ -41,22 +41,22 @@ const btnsConf = [
{
label: '清空',
action: btn_type.CLEAR,
icon: '/static/v-sign/clear.png'
icon: require('../../static/image/clear.svg')
},
{
label: '撤回',
action: btn_type.PREV,
icon: '/static/v-sign/prev.png'
icon: require('../../static/image/prev.svg')
},
// {
// label: '',
// action: btn_type.NEXT,
// icon: '/static/v-sign/next.png'
// icon: '/uni_modules/v-sign/static/image/next.svg'
// },
{
label: '保存',
action: btn_type.SAVE,
icon: '/static/v-sign/save.png'
icon: require('../../static/image/save.svg')
}
]
export default {
@ -83,7 +83,7 @@ export default {
default: () => ({})
}
},
inject: ['getInterface'],
inject: ['getSignInterface'],
data() {
return {
formatSize
@ -95,11 +95,10 @@ export default {
}
},
mounted() {
vSignInterface = this.getInterface()
vSignInterface = this.getSignInterface()
},
methods: {
async onBtnClick(btn) {
// console.log(btn, btn.action)
let emit_result
switch (btn.action) {
case btn_type.CLEAR:
@ -112,7 +111,7 @@ export default {
// console.log('next')
// break
case btn_type.SAVE:
emit_result = await vSignInterface.canvasToTempFilePath()
emit_result = await vSignInterface.saveImage()
break
default:
break
@ -138,8 +137,12 @@ export default {
&:last-child {
margin-right: 0;
}
&:active {
transition: transform 0.3s;
transform: scale(1.1);
}
&.border {
border: 2rpx solid #666;
border: 2rpx solid #333;
border-radius: 12rpx;
}
.icon {
@ -154,7 +157,7 @@ export default {
}
}
.text {
color: #666;
color: #333;
font-size: 28rpx;
}
}

View File

@ -0,0 +1,212 @@
<template>
<view class="v-sign-color">
<view class="options">
<view
class="option-item"
v-for="(color, index) in options"
:key="color"
:style="[optionStyle(color, index)]"
@click="onSelect(color, index)"
>
<view class="option-inner" :style="[optionInnerStyle(color, index)]">
{{ tick && currentIndex === index ? '✓' : '' }}
</view>
</view>
</view>
</view>
</template>
<script>
/**
* v-sign-color 颜色选择器v-sign 子组件
* @description 设置 v-sign 画笔的颜色
* @tutorial
* @property {String} type 选项样式可选 SQUARE \ CIRCLE
* @property {String} color 默认颜色
* @property {Array} options 备选色
* @property {Number/String} size /方形大小
* @property {Boolean} tick 是否选中打勾
* @property {Number/String} tickSize 勾大小
* @property {String} borderColor 边框颜色
* @property {Boolean} border 是否有边框
* @property {Number/String} space 选项间隙
* @event {Function} change 改变颜色时触发
* @example 示例
**/
import { formatSize } from '../../utils'
//
const type_style = {
CIRCLE: 'circle',
SQUARE: 'square'
}
// v-sign
let vSignInterface
export default {
name: 'v-sign-color',
props: {
//
type: {
type: String,
default: type_style.SQUARE
},
//
color: {
type: String
},
//
options: {
type: Array,
default: () => ['#333', '#f44236', '#3f51b5', '#2195f3', '#ffeb3b', '#ff9900']
},
// /
size: {
type: [Number, String],
default: 44
},
//
tick: {
type: Boolean,
default: true
},
//
tickSize: {
type: [Number, String],
default: 24
},
//
borderColor: {
type: String,
default: '#fff'
},
//
border: {
type: Boolean,
default: false
},
//
space: {
type: [Number, String],
default: 16
}
},
inject: ['getSignInterface'],
data() {
return {
formatSize,
currentIndex: null
}
},
watch: {
color: {
handler(val) {
if (!val) {
this.currentIndex = 0
return
}
const index = this.options.findIndex(item => item === this.color)
if (index > 0) {
this.currentIndex = index
}
},
immediate: true
}
},
computed: {
currentColor() {
return this.options[this.currentIndex]
}
},
mounted() {
vSignInterface = this.getSignInterface()
this.setLineColor()
},
methods: {
onSelect(color, index) {
this.currentIndex = index
this.$emit('change', color)
this.setLineColor()
},
setLineColor() {
vSignInterface.setLineColor(this.currentColor)
},
optionStyle(color, index) {
const size = formatSize(this.size)
let style = {
marginRight: formatSize(this.space),
width: size,
height: size,
lineHeight: size,
border:
this.border && this.currentIndex === index
? `4rpx solid ${this.borderColor}`
: '',
padding: this.currentIndex === index ? 0 : this.border ? '4rpx' : ''
}
switch (this.type) {
case type_style.SQUARE:
style = {
...style,
borderRadius: this.currentIndex === index ? '8rpx' : 0
}
break
case type_style.CIRCLE:
style = {
...style,
borderRadius: '50%'
}
break
}
return style
},
optionInnerStyle(color, index) {
let style = {
background: color,
fontSize: formatSize(this.tickSize)
}
switch (this.type) {
case type_style.SQUARE:
style = {
...style,
borderRadius: this.currentIndex === index ? 0 : '8rpx'
}
break
case type_style.CIRCLE:
style = {
...style,
borderRadius: '50%'
}
break
}
return style
}
}
}
</script>
<style lang="scss" scoped>
.options {
padding: 8rpx;
display: flex;
flex-wrap: wrap;
}
.option-item {
overflow: hidden;
&:last-child {
margin-right: 0;
}
&:active {
transition: transform 0.3s;
transform: scale(1.1);
}
}
.option-inner {
width: 100%;
height: 100%;
text-align: center;
font-weight: bold;
color: #fff;
}
</style>

View File

@ -31,7 +31,7 @@
<script>
/**
* v-sign-pen 画笔v-sign 子组件
* @description 控制 v-sign 画笔的线宽
* @description 设置 v-sign 画笔的线宽
* @tutorial
* @property {String} type 选项样式 line / circle
* @property {String} label 标签
@ -105,7 +105,7 @@ export default {
default: 4
}
},
inject: ['getInterface'],
inject: ['getSignInterface'],
data() {
return {
type_style,
@ -142,7 +142,7 @@ export default {
this.currentSelect = this.csizes[0]
},
mounted() {
vSignInterface = this.getInterface()
vSignInterface = this.getSignInterface()
this.setLineWidth()
},
methods: {
@ -195,6 +195,10 @@ export default {
&:last-child {
margin-right: 0;
}
&:active {
transition: transform 0.3s;
transform: scale(1.1);
}
.circle {
border-radius: 50%;
padding: 4rpx;

View File

@ -0,0 +1,314 @@
<template>
<view class="signature-wrap">
<canvas
:canvas-id="cid"
:id="cid"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
disable-scroll
style="width: 100%;height: 600rpx;"
></canvas>
<slot />
</view>
</template>
<script>
/**
* sign canvas 手写签名
* @description 设置线条宽度颜色撤回清空
* @tutorial
* @property {String} cid canvas id 不设置则默认为 v-sign-时间戳
* @property {String, Number} width canvas 宽度
* @property {String, Number} height canvas 高度
* @property {bgColor} bgColor 画布背景颜色
* @property {Object} customStyle canvas 自定义样式
* @property {String} lineWidth 画笔大小权重小于 v-sign-pen 组件设置的画笔大小
* @property {Number} lineColor 画笔颜色权重小于 v-sign-pen 组件设置的画笔大小
* @event {Function} init 当创建完 canvas 实例后触发向外提供 canvas实例撤回清空方法
* @example <v-sign @init="signInit"></v-sign>
*/
import { formatSize } from '../../utils'
export default {
name: 'v-sign',
props: {
// canvas id
cid: {
type: String,
default: `v-sign-${Date.now()}`
// required: true
},
// canvas
width: {
type: [String, Number]
},
// canvas
height: {
type: [String, Number]
},
// v-sign-pen penLineWidth
lineWidth: {
type: Number,
default: 4
},
// 线 v-sign-color penLineColor
lineColor: {
type: String,
default: '#333'
},
//
bgColor: {
type: String,
default: '#fff'
},
// canvas
customStyle: {
type: Object,
default: () => ({})
}
},
provide() {
return {
getSignInterface: this.provideSignInterface
}
},
data() {
return {
formatSize,
lineData: [],
winWidth: 0,
winHeight: 0,
penLineWidth: null, // v-sign-pen
penLineColor: null // v-sign-color
}
},
created() {
//
const { windowWidth, windowHeight } = uni.getSystemInfoSync()
this.winWidth = windowWidth
this.winHeight = windowHeight
},
mounted() {
this.canvasCtx = uni.createCanvasContext(this.cid, this)
// h5
// #ifdef H5
setTimeout(() => {
// #endif
this.setBackgroundColor(this.bgColor)
// #ifdef H5
}, 10)
// #endif
// init
this.$emit('init', this.provideSignInterface())
},
methods: {
onTouchStart(e) {
const pos = e.touches[0]
this.lineData.push({
style: {
color: this.penLineColor || this.lineColor,
width: this.penLineWidth || this.lineWidth
},
//
coordinates: [
{
type: e.type,
x: pos.x,
y: pos.y
}
]
})
this.drawLine()
},
onTouchMove(e) {
const pos = e.touches[0]
this.lineData[this.lineData.length - 1].coordinates.push({
type: e.type,
x: pos.x,
y: pos.y
})
this.drawLine()
},
onTouchEnd(e) {
this.$emit('end', this.lineData)
},
//
clear() {
this.lineData = []
this.canvasCtx.clearRect(0, 0, this.winWidth, this.winHeight)
this.canvasCtx.draw()
this.setBackgroundColor(this.bgColor)
this.$emit('clear')
},
//
revoke() {
this.setBackgroundColor(this.bgColor)
this.lineData.pop()
this.lineData.forEach((item, index) => {
this.canvasCtx.beginPath()
this.canvasCtx.setLineCap('round')
this.canvasCtx.setStrokeStyle(item.style.color)
this.canvasCtx.setLineWidth(item.style.width)
if (item.coordinates.length < 2) {
const pos = item.coordinates[0]
this.canvasCtx.moveTo(pos.x, pos.y)
this.canvasCtx.lineTo(pos.x + 1, pos.y)
} else {
item.coordinates.forEach(pos => {
if (pos.type == 'touchstart') {
this.canvasCtx.moveTo(pos.x, pos.y)
} else {
this.canvasCtx.lineTo(pos.x, pos.y)
}
})
}
this.canvasCtx.stroke()
})
this.canvasCtx.draw(true)
this.$emit('revoke', this.lineData)
},
// 线
drawLine() {
const lineDataLen = this.lineData.length
if (!lineDataLen) return
const currentLineData = this.lineData[lineDataLen - 1]
const coordinates = currentLineData.coordinates
const coordinatesLen = coordinates.length
if (!coordinatesLen) return
let startPos
let endPos
if (coordinatesLen < 2) {
// only start, no move event
startPos = coordinates[coordinatesLen - 1]
endPos = {
x: startPos.x + 1,
y: startPos.y
}
} else {
startPos = coordinates[coordinatesLen - 2]
endPos = coordinates[coordinatesLen - 1]
}
const style = currentLineData.style
this.canvasCtx.beginPath()
this.canvasCtx.setLineCap('round')
this.canvasCtx.setStrokeStyle(style.color)
this.canvasCtx.setLineWidth(style.width)
this.canvasCtx.moveTo(startPos.x, startPos.y)
this.canvasCtx.lineTo(endPos.x, endPos.y)
// const P1 = this.caculateBezier(startPos, endPos, centerPos)
// console.log(P1.x, P1.y)
// this.canvasCtx.moveTo(startPos.x, startPos.y)
// this.canvasCtx.quadraticCurveTo(P1.x, P1.y, endPos.x, endPos.y)
this.canvasCtx.stroke()
this.canvasCtx.draw(true)
},
// png filename h5
async saveImage(filename = '签名') {
const tempFilePath = await this.canvasToTempFilePath()
return new Promise((resolve, reject) => {
// #ifdef H5
try {
const a = document.createElement('a')
a.href = tempFilePath
a.download = filename
document.body.appendChild(a)
a.click()
a.remove()
resolve({
errMsg: 'saveImageH5:ok'
})
} catch (e) {
console.error(e)
reject(e)
}
// #endif
// #ifndef H5
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success(resObj) {
resolve(resObj)
},
fail(err) {
reject(err)
}
})
// #endif
})
},
// canvas h5 base64
canvasToTempFilePath(conf = {}) {
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath(
{
canvasId: this.cid,
...conf,
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
console.log('fail', err)
reject(err)
}
},
this
)
})
},
setBackgroundColor(color = '#fff') {
this.canvasCtx.beginPath()
this.canvasCtx.setFillStyle(color)
this.canvasCtx.fillRect(0, 0, this.winWidth, this.winHeight)
this.canvasCtx.fill()
this.canvasCtx.draw(true)
},
setLineWidth(numberVal) {
this.penLineWidth = numberVal
},
setLineColor(strValue) {
this.penLineColor = strValue
},
//
provideSignInterface() {
return {
cid: this.cid,
ctx: this.canvasCtx,
clear: this.clear,
revoke: this.revoke,
saveImage: this.saveImage,
canvasToTempFilePath: this.canvasToTempFilePath,
setLineWidth: this.setLineWidth,
setLineColor: this.setLineColor,
setBackgroundColor: this.setBackgroundColor,
getLineData: () => this.lineData
}
},
/**
* 计算二次贝塞尔曲线 控制点 P1
* 起点 P0(x0,y0)控制点P1(x1, y1)P2(x2, y2)曲线上任意点B(x, y)
* 二次贝塞尔公式B(t) = (1-t)²P0 + 2t(1-t)P1 + t²P2
* 代入坐标得
* x = (1-t)²*x0 + 2t(1-t)*x1 + *x2
* y = (1-t)²*y0 + 2t(1-t)*y1 + *y2
*/
caculateBezier(P0, P2, B, t = 0.5) {
const { x: x0, y: y0 } = P0
const { x: x2, y: y2 } = P2
const { x, y } = B
let x1 = (x - (1 - t) * (1 - t) * x0 - t * t * x2) / (2 * t * (1 - t))
let y1 = (y - (1 - t) * (1 - t) * y0 - t * t * y2) / (2 * t * (1 - t))
return {
x: x1,
y: y1
}
}
}
}
</script>
<style lang="scss" scoped>
.signature-wrap {
position: relative;
}
</style>

83
pages/v-sign/package.json Normal file
View File

@ -0,0 +1,83 @@
{
"id": "v-sign",
"displayName": "canvas 手写 签名 签字 画板组件",
"version": "0.5.2",
"description": "基于 canvas 实现1. 支持线条、背景色自定义样式2. 支持撤回、清空、导出图片等功能3. 内置按钮、画笔等子组件。",
"keywords": [
"canvas",
"签名",
"签字",
"电子签名",
"signature"
],
"repository": "https://github.com/jizai1125/v-sign",
"engines": {
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"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"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "n"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"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",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

245
pages/v-sign/readme.md Normal file
View File

@ -0,0 +1,245 @@
# v-sign 手写签名
**如有问题或者建议欢迎留言或加群联系我群号736123963将保持维护**
<img src="https://i.loli.net/2021/12/02/bgsfnDmCzXGq8ct.png" alt="uniapp 交流群群聊二维码" style="zoom: 80%;" />
## 快速使用
基础示例,具体说明见下方 API**若需要使用内置子组件,见下方子组件说明。**
```html
<template>
<v-sign :width="winWidth + 'px'" @init="onSignInit"></v-sign>
<button @click="clear">清空<button>
</template>
<script>
export default {
data() {
return {
winWidth: 0
}
},
created() {
// 宽度设为可使用窗口宽度
const { windowWidth } = uni.getSystemInfoSync()
this.winWidth = windowWidth
},
methods: {
onSignInit(signCtx) {
this.signCtx = signCtx
},
// 清空
clear() {
this.signCtx.clear()
}
}
}
</script>
```
## API
### 属性 (Props)
| 属性名 | 类型 | 默认值 | 说明 |
| :---------: | :-----------: | :-----------: | :--------------------------: |
| cid | String | v-sign-时间戳 | canvas id |
| width | String/Number | - | canvas 宽度Number 单位 rpx |
| height | String/Number | - | canvas 高度Number 单位 rpx |
| customStyle | Object | - | canvas 自定义样式 |
| lineWidth | Number | 4 | 线宽,单位 px |
| lineColor | String | #333 | 线颜色 |
| bgColor | String | #fff | 画布背景颜色 |
### 事件Events
| 事件称名 | 说明 | 返回值 |
| :------: | :------------------------------------------------------------: | :--------------------------------: |
| @init | 创建完 canvas 实例后触发,向外提供 canvas 实例,撤回,清空方法 | Object具体见下方事件回调参数说明 |
| @clear | 清空画布后触发 | - |
| @revoke | 撤销操作后触发 | 坐标信息数组 |
| @end | 每次绘制结束后触发 | 坐标信息数组 |
### 事件回调参数说明
#### **`init(ctx: SignContext)`**
可以通过该事件回调暴露的 clear、revoke 等方法操作画布。
```java
interface SignContext {
// canvas 实例
ctx: object;
// 清空画布
clear(): void;
// 撤回
revoke(): void;
// 保存 png 图片,文件名 filename 配置仅支持 h5
saveImage(filename: string): Promise<object>;
// 返回图片临时文件路径config 参数同 uni.canvasToTempFilePath方法内部只是做了 Promise 化处理而已
canvasToTempFilePath(config: object): Promise<object>;
// 设置画布背景色
setBackgroundColor(color: string): void;
setLineWidth(value: number): void;
setLineColor(value: string): void;
// 获取坐标信息数组
getLineData(): Array<object>;
}
```
示例:
```html
<template>
<v-sign @init="onSignInit"></v-sign>
<button @click="clear">清空<button>
<button @click="revoke">撤回<button>
<button @click="saveTempFilePath">保存临时图片路径<button>
<button @click="saveImage">保存图片<button>
</template>
<script>
export default {
methods: {
onSignInit(signCtx) {
this.signCtx = signCtx
},
// 清空
clear() {
this.signCtx.clear()
},
// 撤回
revoke() {
this.signCtx.revoke()
},
// 保存为临时图片路径h5返回 base64
saveTempFilePath() {
this.signCtx.canvasToTempFilePath()
},
// 保存 png 图片
saveImage() {
this.signCtx.saveImage()
}
}
}
</script>
```
# 子组件
子组件需要包裹在 `v-sign` 组件内使用!!!
## 按钮控件v-sign-action
### 示例
```html
<template>
<v-sign>
<v-sign-action @save="save" @clear="clear" @prev="revoke"></v-sign-action>
</v-sign>
</template>
<script>
export default {
methods: {
save(tempFilePath) {
console.log(tempFilePath);
},
},
};
</script>
```
### API
### 属性 (Props)
| 属性名 | 类型 | 默认值 | 说明 |
| :---------: | :-----------: | :-----------------------: | :----------------------------------------------------: |
| actions | Array | ["clear", "prev", "save"] | 按钮配置清空clear, 撤回prev 保存图片save |
| border | Boolean | true | 按钮是否有边框 |
| space | String/Number | 12 | 按钮间隔Number 单位 rpx |
| customStyle | Object | - | 根元素自定义样式 |
### 事件Events
点击对应类型按钮触发对应事件, 例如配置了清空clear按钮点击则触发 clear 事件。
---
## 画笔组件v-sign-pen
### 示例
```html
<template>
<v-sign>
<v-sign-pen></v-sign-pen>
</v-sign>
</template>
<script>
export default {};
</script>
```
### API
### 属性 (Props)
| 属性名 | 类型 | 默认值 | 说明 |
| :---------: | :-----: | :--------------: | :--------------------------: |
| type | String | circle | 选项样式,可选 circle \ line |
| label | String | - | 标签 |
| sizes | Array | [2, 4, 6, 8, 10] | 画笔尺寸数组,单位 px |
| color | String | #333 | 选项颜色 |
| activeColor | String | #333 | 选中项颜色 |
| border | Boolean | true | 选中项是否有边框 |
| borderWidth | Number | 4 | 边框大小,单位 rpx |
| space | Number | 20 | 选项间隙,单位 rpx |
| bigger | Number | 2 | 圆点变大变粗倍数 |
| minSize | Number | 4 | 圆点最小尺寸,单位 px |
### 事件Events
| 事件称名 | 说明 | 返回值 |
| :------: | :----------------: | :----------------: |
| @change | 选择画笔大小时触发 | size画笔尺寸大小 |
---
## 颜色选择器组件v-sign-color
### 示例
```html
<template>
<v-sign>
<v-sign-color></v-sign-color>
</v-sign>
</template>
<script>
export default {};
</script>
```
### API
### 属性 (Props)
| 属性名 | 类型 | 默认值 | 说明 |
| :---------: | :-----------: | :-------------------------------------------------------------: | :-------------------: |
| type | String | square | 选项样式,可选 circle |
| color | String | - | 默认颜色 |
| options | Array | ['#333', '#f44236', '#3f51b5', '#2195f3', '#ffeb3b', '#ff9900'] | 备选色 |
| size | Number/String | 44 | 圆/方形大小,单位 rpx |
| tick | Boolean | true | 是否选中打勾 |
| tickSize | Number/String | 24 | 勾大小 |
| borderColor | String | #fff | 边框颜色 |
| border | Boolean | false | 是否有边框 |
| space | Number/String | 16 | 选项间隙 |
### 事件Events
| 事件称名 | 说明 | 返回值 |
| :------: | :------------: | :----: |
| @change | 选择颜色时触发 | color |

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1639214313546" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="827" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M274.56 798.997333l19.434667-25.130666-33.792 68.565333a18.133333 18.133333 0 0 0 11.562666 25.536l59.733334 16a18.133333 18.133333 0 0 0 17.28-4.48c20.522667-19.818667 35.626667-35.989333 45.290666-48.469333l19.456-25.130667-33.813333 68.565333a18.133333 18.133333 0 0 0 11.562667 25.536l84.48 22.634667a18.133333 18.133333 0 0 0 17.28-4.48c20.522667-19.84 35.626667-35.989333 45.269333-48.469333l19.456-25.130667-33.813333 68.565333A18.133333 18.133333 0 0 0 535.530667 938.666667l72.106666 19.328a18.133333 18.133333 0 0 0 17.28-4.48c20.522667-19.84 35.626667-36.010667 45.269334-48.490667l19.456-25.130667-33.813334 68.586667a18.133333 18.133333 0 0 0 11.584 25.514667l86.421334 23.338666 3.84-0.213333c13.269333-0.704 29.056-5.034667 43.84-12.8 29.781333-15.701333 48.170667-43.2 52.181333-78.250667 2.133333-18.517333 4.778667-38.549333 8.405333-63.530666 1.642667-11.221333 2.944-20.010667 6.229334-41.834667 11.050667-73.322667 14.634667-101.034667 17.130666-133.674667l0.938667-12.373333 2.837333-2.922667 12.330667-1.344a41.813333 41.813333 0 0 0 24.810667-11.221333c10.730667-10.24 14.805333-25.386667 11.093333-42.197333l-37.546667-171.584c-3.029333-13.696-11.264-27.946667-23.146666-39.829334-11.648-11.626667-25.92-20.138667-39.893334-23.893333L723.626667 331.306667l-2.261334-3.925334L774.250667 130.133333c8.32-31.061333-11.754667-63.744-44.970667-72.64l-79.509333-21.312c-33.194667-8.896-66.922667 9.365333-75.264 40.426667l-52.842667 197.269333-3.925333 2.261334-118.101334-31.637334c-13.994667-3.754667-30.634667-3.498667-46.506666 0.746667-16.256 4.352-30.506667 12.586667-39.957334 22.933333l-118.314666 129.792c-11.605333 12.714667-15.658667 27.84-11.52 42.090667 4.16 14.229333 15.850667 25.194667 32.896 30.528l13.610666 4.266667 2.133334 3.882666-3.626667 13.802667c-21.12 79.850667-52.885333 136.917333-85.717333 150.890667-47.530667 20.202667-72.938667 49.429333-78.421334 85.034666-5.034667 32.682667 9.28 67.114667 37.589334 91.541334l22.037333 8.341333 74.666667 20.010667a42.666667 42.666667 0 0 0 41.216-11.050667c15.274667-15.274667 26.88-28.032 34.837333-38.293333z m551.381333-396.565333c14.144 3.797333 29.952 19.2 32.768 32l34.56 157.781333a10.666667 10.666667 0 0 1-13.184 12.586667L240.64 433.493333a10.666667 10.666667 0 0 1-5.12-17.493333l108.8-119.36c8.832-9.685333 30.229333-15.146667 44.373333-11.349333l141.333334 37.866666a21.333333 21.333333 0 0 0 26.133333-15.082666l58.304-217.642667a21.333333 21.333333 0 0 1 26.133333-15.082667l77.056 20.650667a21.333333 21.333333 0 0 1 15.082667 26.133333l-58.325333 217.642667a21.333333 21.333333 0 0 0 15.082666 26.112l136.448 36.565333zM315.456 701.568c-33.664 45.141333-64.597333 79.082667-92.8 101.802667l-5.909333 4.778666-2.837334 0.597334-88.106666-24.106667-2.922667-3.2c-13.034667-14.165333-19.370667-31.04-16.981333-46.592 3.285333-21.333333 22.058667-39.338667 53.205333-52.586667 31.722667-13.482667 59.818667-47.104 82.922667-99.904 10.026667-22.954667 18.88-48.725333 26.389333-76.586666l3.882667-14.4 3.904-2.261334 566.165333 151.701334 2.346667 3.306666-0.789334 12.224c-1.984 30.592-30.336 229.397333-32.128 244.906667-2.346667 20.416-11.306667 34.986667-27.605333 44.394667a73.237333 73.237333 0 0 1-21.397333 8.106666l-5.013334 0.725334-60.373333-16.170667 11.242667-20.288c8.277333-14.976 22.656-43.84 43.093333-86.613333a21.12 21.12 0 0 0-9.962667-28.16l-3.136-1.493334a21.333333 21.333333 0 0 0-26.261333 6.485334c-33.642667 45.056-64.533333 78.912-92.672 101.546666l-5.909333 4.757334-2.837334 0.597333-52.544-14.08 11.114667-20.266667c3.562667-6.485333 7.04-13.013333 10.453333-19.626666 7.04-13.504 17.898667-35.797333 32.597334-66.816a21.290667 21.290667 0 0 0-9.984-28.309334l-3.029334-1.450666a21.333333 21.333333 0 0 0-26.368 6.442666c-33.6 45.013333-64.469333 78.826667-92.608 101.482667l-5.909333 4.757333-2.837333 0.597334-52.138667-13.973334 11.114667-20.266666c3.242667-5.888 6.72-12.416 10.453333-19.626667 6.997333-13.461333 17.962667-35.946667 32.896-67.434667a20.970667 20.970667 0 0 0-10.112-28.010666l-3.328-1.536a21.333333 21.333333 0 0 0-26.069333 6.613333c-33.642667 45.056-64.554667 78.976-92.778667 101.696l-5.909333 4.757333-2.837334 0.597334-32.64-8.746667 11.093334-20.245333c3.541333-6.506667 7.04-13.034667 10.453333-19.626667 6.976-13.482667 17.941333-35.968 32.874667-67.456a21.056 21.056 0 0 0-10.069334-28.074667l-3.242666-1.514666a21.333333 21.333333 0 0 0-26.154667 6.549333z" fill="#333333" p-id="828"></path></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1639214327673" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1119" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M588.8 934.4c-17.066667-4.266667-25.6-21.333333-25.6-38.4v-204.8c-68.266667-8.533333-128-4.266667-192 12.8-76.8 21.333333-170.666667 93.866667-273.066667 213.333333-12.8 17.066667-34.133333 21.333333-51.2 12.8-17.066667-8.533333-29.866667-25.6-25.6-42.666666 17.066667-162.133333 76.8-298.666667 187.733334-405.333334 98.133333-98.133333 213.333333-153.6 349.866666-166.4V128c0-17.066667 8.533333-34.133333 25.6-38.4 17.066667-8.533333 34.133333-4.266667 46.933334 8.533333l358.4 375.466667c17.066667 17.066667 17.066667 42.666667 0 59.733333l-358.4 392.533334c-8.533333 8.533333-21.333333 12.8-29.866667 12.8-4.266667 0-8.533333 0-12.8-4.266667z m55.466667-699.733333v123.733333c0 21.333333-17.066667 42.666667-42.666667 42.666667-132.266667 0-243.2 46.933333-337.066667 140.8-59.733333 59.733333-102.4 128-128 204.8 72.533333-68.266667 145.066667-110.933333 209.066667-128 85.333333-21.333333 166.4-25.6 260.266667-8.533334 21.333333 4.266667 34.133333 21.333333 34.133333 42.666667v132.266667l256-281.6-251.733333-268.8z" fill="#333333" p-id="1120"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1639214320974" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="973" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M422.4 938.666667c-12.8 0-21.333333-4.266667-29.866667-12.8L34.133333 533.333333c-17.066667-17.066667-12.8-42.666667 0-59.733333l358.4-375.466667c8.533333-12.8 25.6-17.066667 42.666667-8.533333s25.6 21.333333 25.6 38.4v187.733333c136.533333 8.533333 256 64 354.133333 166.4 106.666667 106.666667 170.666667 243.2 187.733334 405.333334 0 17.066667-8.533333 38.4-25.6 42.666666-17.066667 8.533333-38.4 4.266667-51.2-12.8-98.133333-119.466667-192-196.266667-273.066667-213.333333-64-17.066667-123.733333-21.333333-192-12.8V896c0 17.066667-12.8 34.133333-25.6 38.4-4.266667 4.266667-8.533333 4.266667-12.8 4.266667z m-298.666667-435.2l256 281.6v-132.266667c0-21.333333 17.066667-38.4 34.133334-42.666667 93.866667-17.066667 174.933333-12.8 260.266666 8.533334 64 17.066667 136.533333 59.733333 209.066667 128-25.6-76.8-72.533333-149.333333-128-204.8-93.866667-93.866667-204.8-140.8-337.066667-140.8-21.333333 0-42.666667-21.333333-42.666666-42.666667V234.666667l-251.733334 268.8z" fill="#333333" p-id="974"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1639214153477" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="861" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M62.848 60.992h704v64h-704zM62.848 640.832h543.936v64H62.848z" fill="#333333" p-id="862"></path><path d="M702.848 60.992h64v320h-64zM62.848 113.536h64v527.296h-64z" fill="#333333" p-id="863"></path><path d="M70.528 498.304l218.24-117.312 30.4 56.32-218.304 117.376z" fill="#333333" p-id="864"></path><path d="M298.432 375.04l144.896 274.176-56.576 29.888L241.92 404.928zM512 194.176h126.592v127.168H512zM663.296 448.448h128v320h-128z" fill="#333333" p-id="865"></path><path d="M599.36 764.992l127.936 128 128.064-128z" fill="#333333" p-id="866"></path></svg>

After

Width:  |  Height:  |  Size: 934 B

View File

@ -10,6 +10,6 @@ export function isNumber(val) {
* 处理大小单位
* @param {Object} val
*/
export function formatSize(val) {
return isNumber(val) ? `${val}rpx` : val
export function formatSize(val, unit = 'rpx') {
return isNumber(val) ? `${val}${unit}` : val
}