效果图
![在这里插入图片描述](https://img-blog.csdnimg.cn/202105141554127.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgyNjEzNg==,size_16,color_FFFFFF,t_70#pic_center)
<template>
<canvas
:ref="domId"
class="app-sign-canvas" :id="domId"
@mousedown.prevent.stop="handleMousedown"
@mousemove.prevent.stop="handleMousemove"
@mouseup.prevent.stop="handleMouseup"
@mouseleave.prevent.stop="handleMouseleave"
@touchstart.prevent.stop="handleTouchstart"
@touchmove.prevent.stop="handleTouchmove"
@touchend.prevent.stop="handleTouchend">
</canvas>
</template>
<script>
export default {
name: 'SignCanvas',
model: {
value: 'image',
event: 'confirm'
},
props: {
image: {
required: false,
type: [String],
default: null
},
options: { //配置项
required: false,
type: [Object],
default: () => null
},
},
data () {
return {
value: null, //base64
domId: `sign-canvas-${Math.random().toString(36).substr(2)}`, //生成唯一dom标识
canvas:null, //canvas dom对象
context:null, //canvas 画布对象
dpr: 1,
config: {
isDpr: false, //是否使用dpr兼容高分屏 [Boolean] 可选
lastWriteSpeed: 1, //书写速度 [Number] 可选
lastWriteWidth: 2, //下笔的宽度 [Number] 可选
lineCap: 'round', //线条的边缘类型 [butt]平直的边缘 [round]圆形线帽 [square] 正方形线帽
lineJoin: 'round', //线条交汇时边角的类型 [bevel]创建斜角 [round]创建圆角 [miter]创建尖角。
canvasWidth: 350, //canvas宽高 [Number] 可选
canvasHeight: 300, //高度 [Number] 可选
isShowBorder: true, //是否显示边框 [可选]
bgColor: '#fcc', //背景色 [String] 可选 注:这背景色仅仅只是canvas背景,保存的图片仍然是透明的
borderWidth: 1, // 网格线宽度 [Number] 可选
borderColor: "#ff787f", //网格颜色 [String] 可选
writeWidth: 5, //基础轨迹宽度 [Number] 可选
maxWriteWidth: 30, // 写字模式最大线宽 [Number] 可选
minWriteWidth: 5, // 写字模式最小线宽 [Number] 可选
writeColor: '#101010', // 轨迹颜色 [String] 可选
isSign: false, //签名模式 [Boolean] 默认为非签名模式,有线框, 当设置为true的时候没有任何线框
imgType:'png' //下载的图片格式 [String] 可选为 jpeg canvas本是透明背景的
}
};
},
mounted () {
this.init();
},
watch:{
options:{
handler(){
this.init();
},
deep: true
}
},
methods: {
init () {
const options = this.options;
if (options) {
for (const key in options) {
this.config[key] = options[key];
}
}
this.dpr = typeof window !== 'undefined' && this.config.isDpr ? window.devicePixelRatio || window.webkitDevicePixelRatio || window.mozDevicePixelRatio || 1 : 1;
this.canvas = document.getElementById(this.domId);
this.context = this.canvas.getContext("2d");
this.canvas.style.background = this.config.bgColor;
this.canvas.height = this.config.canvasWidth;
this.canvas.width = this.config.canvasHeight;
this.canvasInit();
this.canvasClear();
},
/**
* 轨迹宽度
*/
setLineWidth () {
const nowTime = new Date().getTime();
const diffTime = nowTime - this.config.lastWriteTime;
this.config.lastWriteTime = nowTime;
let returnNum = this.config.minWriteWidth + (this.config.maxWriteWidth - this.config.minWriteWidth) * diffTime / 30;
if (returnNum < this.config.minWriteWidth) {
returnNum = this.config.minWriteWidth;
} else if (returnNum > this.config.maxWriteWidth) {
returnNum = this.config.maxWriteWidth;
}
returnNum = returnNum.toFixed(2);
//写字模式和签名模式
if (this.config.isSign) {
this.context.lineWidth = this.config.writeWidth * this.dpr;
} else {
const lineWidth = this.config.lastWriteWidth = this.config.lastWriteWidth / 4 * 3 + returnNum / 4
this.context.lineWidth = lineWidth * this.dpr;
}
},
/**
* 开始
*/
writeBegin (point) {
this.config.isWrite = true;
this.config.lastWriteTime = new Date().getTime();
this.config.lastPoint = point;
this.writeContextStyle();
},
/**
* 绘制轨迹
*/
writing (point) {
this.context.beginPath();
this.context.moveTo(this.config.lastPoint.x * this.dpr, this.config.lastPoint.y * this.dpr);
this.context.lineTo(point.x * this.dpr, point.y * this.dpr);
this.setLineWidth();
this.context.stroke();
this.config.lastPoint = point;
this.context.closePath();
},
/**
* 结束
*/
writeEnd (point) {
this.config.isWrite = false;
this.config.lastPoint = point;
this.saveAsImg();
},
/**
* 轨迹样式
*/
writeContextStyle () {
this.context.beginPath();
this.context.strokeStyle = this.config.writeColor;
this.context.lineCap = this.config.lineCap;
this.context.lineJoin = this.config.lineJoin;
},
/**
* 清空
*/
canvasClear () {
this.context.save();
this.context.strokeStyle = this.config.writeColor;
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.beginPath();
this.context.lineWidth = this.config.borderWidth * this.dpr;
this.context.strokeStyle = this.config.borderColor;
let size = this.config.borderWidth / 2 * this.dpr;
if(this.config.isShowBorder){
//画外面的框
this.context.moveTo(size, size);
this.context.lineTo(this.canvas.width - size, size);
this.context.lineTo(this.canvas.width - size, this.canvas.height - size);
this.context.lineTo(size, this.canvas.height - size);
this.context.closePath();
this.context.stroke();
}
if (this.config.isShowBorder && !this.config.isSign) {
//画里面的框
this.context.moveTo(0, 0);
this.context.lineTo(this.canvas.width, this.canvas.height);
this.context.lineTo(this.canvas.width, this.canvas.height / 2);
this.context.lineTo(0, this.canvas.height / 2);
this.context.lineTo(0, this.canvas.height);
this.context.lineTo(this.canvas.width, 0);
this.context.lineTo(this.canvas.width / 2, 0);
this.context.lineTo(this.canvas.width / 2, this.canvas.height);
this.context.stroke();
}
this.$emit('confirm', null);
this.context.restore();
},
/**
* 保存图片 格式base64
*/
saveAsImg() {
if(this.config.isWrite == undefined){
return "";
}
const image = new Image();
image.src = this.canvas.toDataURL(`image/${this.config.imgType}`);
this.$emit('confirm',image.src);
return image.src;
},
/**
* 初始化画板
*/
canvasInit () {
this.canvas.width = this.config.canvasWidth * this.dpr;
this.canvas.height = this.config.canvasHeight * this.dpr;
this.canvas.style.width = `${this.config.canvasWidth}px`;
this.canvas.style.height = `${this.config.canvasHeight}px`;
this.config.emptyCanvas = this.canvas.toDataURL(`image/${this.config.imgType}`);
},
/**
* 鼠标按下 => 下笔
*/
handleMousedown(e){
this.writeBegin({ x: e.offsetX || e.clientX, y: e.offsetY || e.clientY });
},
/**
* 书写过程 => 下笔书写
*/
handleMousemove(e){
this.config.isWrite && this.writing({ x: e.offsetX, y: e.offsetY });
},
/**
* 鼠标松开 => 提笔
*/
handleMouseup(e){
this.writeEnd({ x: e.offsetX, y: e.offsetY });
},
/**
* 离开书写区域 => 提笔离开
*/
handleMouseleave(e){
this.config.isWrite = false;
this.config.lastPoint = { x: e.offsetX, y: e.offsetY };
},
/* ==========================移动端兼容=Start================================ */
/**
* 手指按下 => 下笔
*/
handleTouchstart(e){
const touch = e.targetTouches[0];
const x = touch.clientX ? touch.clientX - this.getRect().left : touch.pageX - this.offset(touch.target,'left');
const y = touch.clientY ? touch.clientY - this.getRect().top : touch.pageY - this.offset(touch.target,'top');
this.writeBegin({ x, y});
},
/**
* 手指移动 => 下笔书写
*/
handleTouchmove(e){
const touch = e.targetTouches[0];
const x = touch.clientX ? touch.clientX - this.getRect().left : touch.pageX - this.offset(touch.target,'left');
const y = touch.clientY ? touch.clientY - this.getRect().top : touch.pageY - this.offset(touch.target,'top');
this.config.isWrite && this.writing({ x, y });
},
/**
* 手指移动结束 => 提笔离开
*/
handleTouchend(e){
const tcs = e.targetTouches;
const ccs = e.changedTouches;
const touch = tcs && tcs.length && tcs[0] || ccs && ccs.length && ccs[0];
const x = touch.clientX ? touch.clientX - this.getRect().left : touch.pageX - this.offset(touch.target,'left');
const y = touch.clientY ? touch.clientY - this.getRect().top : touch.pageY - this.offset(touch.target,'top');
this.writeEnd({ x, y });
},
/* ==========================移动端兼容=End================================== */
/**
* 下载二维码到本地
*/
downloadSignImg(name) {
const c = document.getElementById(this.domId);
const dataURL = c.toDataURL('image/png');
this.saveFile(dataURL, name ? `${name}.${this.config.imgType}` : `${Date.parse(new Date())}.${this.config.imgType}`);
},
/**
* 保存文件
*/
saveFile(data, filename) {
const saveLink = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
saveLink.href = data;
saveLink.download = filename;
const event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
saveLink.dispatchEvent(event);
},
/**
* 获取画布的元素的大小及其相对于视口的位置
* @return {}
*/
getRect() {
return this.$refs[this.domId].getBoundingClientRect();
},
/**
* 获取dom对象的偏移量 可以获取解决position定位的问题
* @returns number
*/
offset(obj, direction) {
//将top,left首字母大写,并拼接成offsetTop,offsetLeft
const offsetDir = 'offset'+ direction[0].toUpperCase()+direction.substring(1);
let realNum = obj[offsetDir];
let positionParent = obj.offsetParent; //获取上一级定位元素对象
while(positionParent != null){
realNum += positionParent[offsetDir];
positionParent = positionParent.offsetParent;
}
return realNum;
}
}
};
</script>
引入上面组件
import signature from './signature'
components: {
signature //注册
},
data() {
return {
maintainerSignature :''
}
}
html代码:
<p class="left h40">员工签名</p>
<section class="clear"">
<signature class="signatureBox upload_file" ref="SignCanvas" :options="optionSignature" v-model="repairsPngData" />
<div class="btnBox">
<button @click="canvasClear()">清 空</button>
<button @click="saveAsImg()">保 存</button>
</div>
</section>
js方法:
//清除画板
canvasClear() {
this.$refs.SignCanvas.canvasClear();
},
//保存
saveAsImg() {
let _this = this;
const img = _this.$refs.SignCanvas.saveAsImg();
//判断不为空
if (img == "") {
_this.$createToast({
type: 'correct',
txt: '请填写签名并保存签名',
}).show()
return false;
}
let formData = new FormData();
formData.append('file', img);
fileUpload(formData).then(res => {
if (res.data.code == 200) {
// 赋值显示即可
_this.maintainerSignature = res.data.data.fullPath;
_this.$createToast({
txt: '操作成功!',
type: 'txt'
}).show();
}
})
},
css
.btnBox {
padding: .625rem;
text-align: center;
border: none
}
.btnBox button:nth-of-type(1) {
background: transparent;
border-radius: .25rem;
height: 2.125rem;
width: 5rem;
font-size: .875rem;
color: #71b900;
border: 1px solid #71b900;
}
.btnBox button:nth-of-type(2) {
background: #71b900;
color: #fff;
border-radius: .25rem;
height: 2.125rem;
width: 5rem;
border: none;
font-size: .875rem;
}