Files
vue3-element-admin/src/views/demo/signature.vue

187 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import FileAPI 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 FileAPI.upload(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(/:(.*?);/);
if (mime) {
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[1] });
}
};
// 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();
}
</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: 0 20px;
background-color: #fff;
canvas {
border: 1px solid #e6e6e6;
}
header {
display: flex;
flex-flow: row nowrap;
align-items: center;
width: 100%;
margin: 8px;
.eraser-option {
display: flex;
label {
white-space: nowrap;
}
}
}
}
</style>