feat: 签名控件

Former-commit-id: 6d3849898b87f95152faa6e4f35af646d113e445
This commit is contained in:
april
2023-06-05 19:27:52 +08:00
parent 7ad9a9a476
commit 62d1bd7c46
3 changed files with 245 additions and 24 deletions

View File

@@ -0,0 +1,210 @@
<script setup lang="ts">
import { uploadFileApi } from "@/api/file";
const imgUrl = ref("");
const canvas = ref();
let ctx: CanvasRenderingContext2D;
// 正在绘制中,用来控制 move 和 end 事件
let painting = false;
// 获取触发点相对被触发dom的左、上角距离
const getOffset = (event: MouseEvent | TouchEvent) => {
let offset: [number, number];
if ((event as MouseEvent).offsetX) {
// pc端
const { offsetX, offsetY } = event as MouseEvent;
offset = [offsetX, offsetY];
} else {
// 移动端
const { top, left } = canvas.value.getBoundingClientRect();
const offsetX = (event as TouchEvent).touches[0].clientX - left;
const offsetY = (event as TouchEvent).touches[0].clientY - top;
offset = [offsetX, offsetY];
}
return offset;
};
// 绘制起点
let startX = 0,
startY = 0;
// 鼠标/触摸 按下时,保存 触发点相对被触发dom的左、上 距离
const onEventStart = (event: MouseEvent | TouchEvent) => {
[startX, startY] = getOffset(event);
painting = true;
};
const onEventMove = (event: MouseEvent | TouchEvent) => {
if (painting) {
// 鼠标/触摸 移动时,保存 移动点相对 被触发dom的左、上 距离
const [endX, endY] = getOffset(event);
paint(startX, startY, endX, endY, ctx);
// 每次绘制 或 清除结束后,起点要重置为上次的终点
startX = endX;
startY = endY;
}
};
const onEventEnd = () => {
if (painting) {
painting = false; // 停止绘制
}
};
onMounted(() => {
ctx = canvas.value.getContext("2d") as CanvasRenderingContext2D;
});
const handleToFile = async () => {
if (isCanvasBlank(canvas.value)) {
ElMessage({
type: "warning",
message: "当前签名文件为空",
});
return;
}
const file = dataURLtoFile(canvas.value.toDataURL(), "签名.png");
if (!file) return;
const { data } = await uploadFileApi(file);
handleClearSign();
imgUrl.value = data.url;
};
const handleClearSign = () => {
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
};
const isCanvasBlank = (canvas: HTMLCanvasElement) => {
const blank = document.createElement("canvas"); //系统获取一个空canvas对象
blank.width = canvas.width;
blank.height = canvas.height;
return canvas.toDataURL() == blank.toDataURL(); //比较值相等则为空
};
// 保存为图片
const handleSaveImg = () => {
if (isCanvasBlank(canvas.value)) {
ElMessage({
type: "warning",
message: "当前签名文件为空",
});
return;
}
const el = document.createElement("a");
// 设置 href 为图片经过 base64 编码后的字符串,默认为 png 格式
el.href = canvas.value.toDataURL();
el.download = "签名";
// 创建一个点击事件并对 a 标签进行触发
const event = new MouseEvent("click");
el.dispatchEvent(event);
};
// 转为file格式可传递给后端
const dataURLtoFile = (dataurl: string, filename: string) => {
const arr: string[] = dataurl.split(",");
if (!arr.length) return;
const mime = arr[0]?.match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
};
// canvas 画图
function paint(
startX: number,
startY: number,
endX: number,
endY: number,
ctx: CanvasRenderingContext2D
) {
ctx.beginPath();
ctx.globalAlpha = 1;
ctx.lineWidth = 2;
ctx.strokeStyle = "#000";
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.closePath();
ctx.stroke();
}
// 橡皮
function eraser(
startX: number,
startY: number,
endX: number,
endY: number,
ctx: CanvasRenderingContext2D,
size: number,
shape: "rect" | "circle"
) {
ctx.beginPath();
ctx.globalAlpha = 1;
switch (shape) {
case "rect":
ctx.lineWidth = size;
ctx.strokeStyle = "#fff";
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.closePath();
ctx.stroke();
break;
case "circle":
ctx.fillStyle = "#fff";
ctx.arc(startX, startY, size, 0, 2 * Math.PI);
ctx.fill();
break;
}
}
</script>
<template>
<div class="canvas-dom">
<h3>基于canvas实现的签名组件</h3>
<header>
<el-button type="primary" @click="handleSaveImg">保存为图片</el-button>
<el-button @click="handleToFile"> 保存到后端 </el-button>
<el-button @click="handleClearSign"> 清空签名 </el-button>
</header>
<canvas
ref="canvas"
height="200"
width="500"
@mousedown="onEventStart"
@mousemove.stop.prevent="onEventMove"
@mouseup="onEventEnd"
@touchstart="onEventStart"
@touchmove.stop.prevent="onEventMove"
@touchend="onEventEnd"
>
</canvas>
<img v-if="imgUrl" :src="imgUrl" alt="签名" />
</div>
</template>
<style scoped lang="scss">
.canvas-dom {
width: 100%;
height: 100%;
padding: 0px 20px;
background-color: #fff;
canvas {
border: 1px solid #e6e6e6;
}
header {
width: 100%;
margin: 8px;
display: flex;
flex-flow: row nowrap;
align-items: center;
.eraser-option {
display: flex;
label {
white-space: nowrap;
}
}
}
}
</style>

View File

@@ -16,7 +16,7 @@ const renderTag = (id: any, options: any) => {
//创建tag元素
const ele = document.createElement("div");
ele.classList.add("tag-demo-con");
ele.innerHTML = `<div class="tag-demo">${tagName}</div>`;
ele.innerHTML = `<div class="tag-wrap"><div class="tag">${tagName}</div></div>`;
return ele;
};
@@ -73,13 +73,25 @@ const onBlur = (): void => {
margin: 10px;
:deep {
.tag-demo-con {
.tag-demo {
padding: 4px;
margin: 4px;
border: 1px solid #ccc;
border-radius: 5px;
line-height: 1em;
min-width: 25px;
.tag-wrap {
height: 25px;
line-height: 25px;
.tag {
display: inline-block;
padding: 2px 8px;
box-sizing: border-box;
border-radius: 16px;
background: #d8eeff;
color: #174c76;
border: 1px solid #bbd6ea;
margin: 0 4px;
}
// padding: 4px;
// margin: 4px;
// border: 1px solid #ccc;
// border-radius: 5px;
// line-height: 1em;
// min-width: 25px;
}
}
}