This commit is contained in:
我是个攻城狮
2026-05-27 16:01:15 +08:00
parent dc43370953
commit c3f5f13bb3
953 changed files with 107635 additions and 11 deletions
+15
View File
@@ -0,0 +1,15 @@
## 1.0.62026-03-25
解决小程序中签字时画板跟随滑动的问题
## 1.0.52026-03-20
修复微信小程序端已发现的BUG
## 1.0.42026-03-19
微信小程增加返回临时图片路径代码,方便实现签名图片上传
## 1.0.32026-03-19
修复微信小程序端横屏样式错乱及图片旋转BUG
## 1.0.22024-12-20
处理BUG:微信小程序uni.canvasToTempFilePath(),报错:canvasToTempFilePath: fail canvas is empty
## 1.0.12024-12-20
1、增加横屏签名旋转
2、示例调整
## 1.0.02023-08-06
提交插件
@@ -0,0 +1,530 @@
<template>
<view class="hjy-sign">
<template v-if="orientation == 'abeam'">
<view class="sighButtons">
<view class="sighButtons_button"
style="border: solid 1rpx #999; color: #999;background-color: #fff;margin-top: 100rpx;"
@click="back">
<p class="sighButtons_button_xiao">返回</p>
</view>
<view class="sighButtons_button" style="background: #550000;" @click="clearClick">
<view class="sighButtons_button_xiao">重签</view>
</view>
<view class="sighButtons_button" style="background-color: #0055ff;" @click="finish">
<view class="sighButtons_button_xiao">完成签名</view>
</view>
</view>
</template>
<!-- 兼容PC端与移动端H5模式下渲染原生canvas其他平台使用uni-app canvas -->
<!-- #ifdef H5 -->
<canvas
ref="realCanvas"
:id="'realCanvas'"
:width="canvasWidth"
:height="canvasHeight"
:style="{width: canvasWidth + 'px', height: canvasHeight + 'px', display: isH5 ? 'block' : 'none'}"
style="touch-action: none;"
></canvas>
<!-- #endif -->
<!-- #ifndef H5 -->
<canvas
id="canvas"
canvas-id="canvas"
@touchmove="move"
@touchstart="start"
@error="error"
@touchend="touchend"
:style="{width:canvasWidth + 'px',height:canvasHeight + 'px'}"
></canvas>
<!-- #endif -->
<template v-if="orientation == 'portrait'">
<view class="sighButton ">
<view class="sighButton_button" style="border: solid 1rpx #999; color: #999;background-color: #fff;"
@click="back">
<p class="sighButton_button_xiao">返回</p>
</view>
<view class="sighButton_button" style="background: #999;" @click="clearClick">
<view class="sighButton_button_xiao">重签</view>
</view>
<view class="sighButton_button" style="background-color: #38A99A;" @click="finish">
<view class="sighButton_button_xiao">完成签名</view>
</view>
</view>
</template>
<canvas type="2d" id="rotateCanvas" style="
width:0;
height:0;
overflow:hidden;
display:block;
position:fixed;
top:0;
left:0;
"></canvas>
</view>
</template>
<script>
export default {
name: "hjy-sign",
props: {
orientation: {
type: String,
default: 'portrait'
},
width: {
type: Number,
default: 0
},
height: {
type: Number,
default: 0
},
lineWidth: {
type: Number,
default: 3
},
strokeStyle: {
type: String,
default: "black"
}
},
data() {
return {
canvas: '',
ctx: '',
pr: 0,
canvasWidth: '',
canvasHeight: '',
points: [],
isDrawing: false, // Only for PC H5
isH5: false
};
},
mounted() {
this.isH5 = !!(typeof window !== 'undefined' && typeof document !== 'undefined' && window.navigator);
this.getSystemInfo();
this.$nextTick(()=>{
this.createCanvas();
});
},
methods: {
//================= PC/H5事件兼容 ================//
_handleMouseDown(e) {
this.isDrawing = true;
const rect = this.$refs.realCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.points.push({ X: x, Y: y });
this.ctx.beginPath();
},
_handleMouseMove(e) {
if (!this.isDrawing) return;
const rect = this.$refs.realCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.points.push({ X: x, Y: y });
if (this.points.length >= 2) this.draw();
},
_handleMouseUp(e) {
this.isDrawing = false;
this.points = [];
},
//------触摸开始
start(e) {
// #ifdef H5
return; // 在H5下不使用uni-app事件
// #endif
this.points.push({
X: e.touches[0].x,
Y: e.touches[0].y
});
this.ctx.beginPath();
},
//------移动
move(e) {
// #ifdef H5
return;
// #endif
this.points.push({
X: e.touches[0].x,
Y: e.touches[0].y
});
if (this.points.length >= 2) {
this.draw();
}
},
//------结束
touchend() {
// #ifdef H5
return;
// #endif
this.points = [];
},
//================= 绘制逻辑 ====================//
draw() {
let point1 = this.points[0];
let point2 = this.points[1];
this.points.shift();
this.ctx.moveTo(point1.X, point1.Y);
this.ctx.lineTo(point2.X, point2.Y);
this.ctx.stroke();
// #ifndef H5
this.ctx.draw(true);
// #endif
},
error(e) {
console.log("画布触摸错误" + e);
},
// 点击完成签名
finish() {
let that = this;
// #ifdef H5
if (this.isH5 && this.$refs.realCanvas) {
let dataUrl = this.$refs.realCanvas.toDataURL("image/png");
this.$emit('finish', dataUrl);
} else {
// #endif
uni.canvasToTempFilePath({
canvasId: 'canvas',
success: async (res) => {
console.log('完成签名', res)
let imgData = '';
if (this.orientation == 'abeam') {
//横屏签名图片旋转
// #ifdef H5
imgData = await this.rotateBase64Img(res.tempFilePath, -90);
// #endif
// #ifdef MP-WEIXIN
imgData = await this.rotateBase64ImgWx(that, res.tempFilePath, 270);
// #endif
} else {
imgData = res.tempFilePath;
}
this.$emit("finish", imgData);
}
}, this)
// #ifdef H5
}
// #endif
},
//重签
clearClick() {
// #ifdef H5
if (this.isH5 && this.$refs.realCanvas) {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
return;
}
// #endif
// #ifndef H5
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.draw(true);
// #endif
},
// 返回事件
back() {
uni.navigateBack();
},
createCanvas() {
// #ifdef H5
if (this.isH5 && this.$refs.realCanvas) {
this.ctx = this.$refs.realCanvas.getContext('2d');
this.ctx.lineJoin = 'round';
this.ctx.lineCap = 'round';
this.ctx.lineWidth = this.lineWidth;
this.ctx.strokeStyle = this.strokeStyle;
this.$refs.realCanvas.addEventListener('mousedown', this._handleMouseDown);
this.$refs.realCanvas.addEventListener('mousemove', this._handleMouseMove);
document.addEventListener('mouseup', this._handleMouseUp);
// 兼容触摸屏的PC
this.$refs.realCanvas.addEventListener('touchstart', this._pcTouchstart, { passive: false });
this.$refs.realCanvas.addEventListener('touchmove', this._pcTouchmove, { passive: false });
this.$refs.realCanvas.addEventListener('touchend', this._pcTouchend, { passive: false });
} else {
// #endif
const pr = this.pr;
const ctx = uni.createCanvasContext('canvas', this);
ctx.lineGap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.strokeStyle;
this.ctx = ctx;
// #ifdef H5
}
// #endif
},
// PC端为触摸屏的兼容
_pcTouchstart(e) {
e.preventDefault();
if (e.touches.length !== 1) return;
const rect = this.$refs.realCanvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
this.isDrawing = true;
this.points.push({ X: x, Y: y });
this.ctx.beginPath();
},
_pcTouchmove(e) {
e.preventDefault();
if (!this.isDrawing) return;
const rect = this.$refs.realCanvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
this.points.push({ X: x, Y: y });
if (this.points.length >= 2) this.draw();
},
_pcTouchend(e) {
e.preventDefault();
this.isDrawing = false;
this.points = [];
},
//src - 图片路径,deg旋转角度
async rotateBase64Img(src, edg) {
return new Promise((resolve, reject) => {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var imgW; //图片宽度
var imgH; //图片高度
var size; //canvas初始大小
if (edg % 90 != 0) {
console.error("旋转角度必须是90的倍数!");
throw '旋转角度必须是90的倍数!';
}
(edg < 0) && (edg = (edg % 360) + 360)
const quadrant = (edg / 90) % 4;
const cutCoor = {
sx: 0,
sy: 0,
ex: 0,
ey: 0
};
var image = new Image();
image.crossOrigin = "anonymous"
image.src = src;
image.onload = () => {
imgW = image.width;
imgH = image.height;
size = imgW > imgH ? imgW : imgH;
canvas.width = size * 2;
canvas.height = size * 2;
switch (quadrant) {
case 0:
cutCoor.sx = size;
cutCoor.sy = size;
cutCoor.ex = size + imgW;
cutCoor.ey = size + imgH;
break;
case 1:
cutCoor.sx = size - imgH;
cutCoor.sy = size;
cutCoor.ex = size;
cutCoor.ey = size + imgW;
break;
case 2:
cutCoor.sx = size - imgW;
cutCoor.sy = size - imgH;
cutCoor.ex = size;
cutCoor.ey = size;
break;
case 3:
cutCoor.sx = size;
cutCoor.sy = size - imgW;
cutCoor.ex = size + imgH;
cutCoor.ey = size + imgW;
break;
}
ctx.translate(size, size);
ctx.rotate(edg * Math.PI / 180);
ctx.drawImage(image, 0, 0);
var imgData = ctx.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey);
if (quadrant % 2 == 0) {
canvas.width = imgW;
canvas.height = imgH;
} else {
canvas.width = imgH;
canvas.height = imgW;
}
ctx.putImageData(imgData, 0, 0);
resolve(canvas.toDataURL())
};
})
},
async rotateBase64ImgWx(that, tempFilePath, rotate) {
return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(that);
query
.select('#rotateCanvas')
.fields({
node: true,
size: true
})
.exec(async (res) => {
try {
if (!res[0] || !res[0].node) {
return reject("未找到 canvas 节点");
}
const canvas = res[0].node;
const ctx = canvas.getContext("2d");
const img = canvas.createImage();
img.src = tempFilePath;
img.onload = () => {
const w = img.width;
const h = img.height;
canvas.width = 1;
canvas.height = 1;
let cw, ch;
if (rotate === 90 || rotate === 270) {
cw = h;
ch = w;
} else {
cw = w;
ch = h;
}
canvas.width = cw;
canvas.height = ch;
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, cw, ch);
ctx.save();
ctx.translate(cw / 2, ch / 2);
ctx.rotate((rotate * Math.PI) / 180);
ctx.drawImage(img, -w / 2, -h / 2, w, h);
ctx.restore();
setTimeout(() => {
uni.canvasToTempFilePath({
canvas: canvas,
width: cw,
height: ch,
destWidth: cw,
destHeight: ch,
fileType: "png",
quality: 0.9,
success: (res) => {
resolve(res.tempFilePath);
},
fail: (err) => {
reject("生成图片失败:" + err.errMsg);
}
}, that);
}, 60);
};
img.onerror = (e) => {
reject("图片加载失败");
};
} catch (e) {
reject("绘制异常:" + e.message);
}
});
});
},
// 获取系统信息
getSystemInfo() {
// #ifdef H5
if (this.isH5) {
this.pr = window.devicePixelRatio || 1;
let windowWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
let windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
if (this.orientation == 'portrait') {
this.canvasWidth = this.width > windowWidth || this.width == 0 ? windowWidth : this.width;
this.canvasHeight = this.height > windowHeight - 70 || this.height == 0 ? windowHeight - 70 : this.height;
} else if (this.orientation == 'abeam') {
this.canvasWidth = this.width > windowWidth - 71 || this.width == 0 ? windowWidth - 71 : this.width;
this.canvasHeight = this.height > windowHeight || this.height == 0 ? windowHeight : this.height;
}
} else {
// #endif
uni.getSystemInfo({
success: (res) => {
this.pr = res.pixelRatio;
if (this.orientation == 'portrait') {
this.canvasWidth = this.width > res.windowWidth || this.width == 0 ? res.windowWidth : this.width;
this.canvasHeight = this.height > res.windowHeight - 70 || this.height == 0 ? res.windowHeight - 70 : this.height;
} else if (this.orientation == 'abeam') {
this.canvasWidth = this.width > res.windowWidth - 71 || this.width == 0 ? res.windowWidth - 71 : this.width;
this.canvasHeight = this.height > res.windowHeight || this.height == 0 ? res.windowHeight : this.height;
}
}
})
// #ifdef H5
}
// #endif
}
},
beforeDestroy() {
// #ifdef H5
if (this.isH5 && this.$refs.realCanvas) {
this.$refs.realCanvas.removeEventListener('mousedown', this._handleMouseDown);
this.$refs.realCanvas.removeEventListener('mousemove', this._handleMouseMove);
document.removeEventListener('mouseup', this._handleMouseUp);
this.$refs.realCanvas.removeEventListener('touchstart', this._pcTouchstart);
this.$refs.realCanvas.removeEventListener('touchmove', this._pcTouchmove);
this.$refs.realCanvas.removeEventListener('touchend', this._pcTouchend);
}
// #endif
}
}
</script>
<style lang="scss">
page {
width: 100%;
height: 100%;
background-color: #e9f2f1;
}
canvas {
background-color: white;
}
.hjy-sign {
width: 100%;
height: 100%;
// touch-action: none; // 移除全局touch-action限制,PC鼠标可正常绘制
display: flex;
flex-wrap: wrap;
align-items: flex-end;
}
.sighButton {
width: 690rpx;
margin: 0rpx auto;
height: 60px;
padding-top: 10px;
display: flex;
justify-content: space-between;
&_button {
width: 30%;
height: 100rpx;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #fff;
}
}
.sighButtons {
width: 70px;
margin: 0rpx auto;
height: 600rpx;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
&_button {
width: 200rpx;
height: 45px;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #fff;
transform: rotate(90deg);
}
}
</style>
+97
View File
@@ -0,0 +1,97 @@
{
"id": "hjy-sign",
"displayName": "hjy-sign",
"version": "1.0.6",
"description": "电子签名、签字板,画板,写字板",
"keywords": [
"电子签名、签字板,画板,写字板"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0",
"uni-app": "^4.87",
"uni-app-x": "^4.87"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "x",
"aliyun": "x",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "-"
},
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "√",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
# hjy-sign
### 基本用法
在 ``template`` 中使用组件
```html
<!-- 基础用法 -->
<view class="sign">
<hjy-sign @finish="finish" orientation="portrait"></hjy-sign>
</view>
```
### 属性
|参数 |描述 |类型 |默认值 |
|:-: |:-: |:-: |:-: |
|orientation|签字板方向。abeam ==> 横向 portrait ==> 纵向 默认纵向 |string |portrait |
|width |签字板宽度。单位PX,宽度定义:恒为正向手机宽度。当宽度大于最大宽度时,设置属性不生效,为屏幕最大宽度.。不传默认为最大宽度 |number |0 |
|height |签字板高度。单位PX,高度定义:恒为正向手机高度。当高度大于最大高度时,设置属性不生效,为屏幕最大高度.。不传默认为最大高度 |number |0 |
|lineWidth |线条粗细 |number |3 |
|strokeStyle|线条颜色 |string |black |
### 事件
|方法 |描述 |类型 |
|:-: |:-: |:-: |
|finish |完成返回画板内容(base64格式) |function |