下面是自封装适用于各个平台的组件代码
<template>
<view class="container">
<view class="canvas_container">
<canvas
canvas-id="signCanvas"
id="signCanvas"
class="canvas"
:disable-scroll="true"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
</view>
<view class="controls">
<input type="color" v-model="color" class="color-picker" />
<view class="width-control">
<button
v-for="w in widths"
:key="w"
:class="{ active: penWidth === w }"
@click="penWidth = w"
>
{{ w }} px
</button>
</view>
<button @click="undo" :disabled="!canUndo">撤销</button>
<button @click="redo" :disabled="!canRedo">重做</button>
<button @click="clearCanvas">清除</button>
<button @click="saveImage">保存</button>
<button @click="exportBase64">导出Base64</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
canvasId: "signCanvas",
color: "#000000",
penWidth: 2,
widths: [1, 2, 4, 6, 8],
isDrawing: false,
lastPoint: null,
currentLine: null,
lines: [],
undoneLines: [],
ctx: null,
canvasWidth: 0,
canvasHeight: 0,
animationFrameId: null,
canvasRect: null,
dpr: 1,
};
},
computed: {
canUndo() {
return this.lines.length > 0;
},
canRedo() {
return this.undoneLines.length > 0;
},
},
mounted() {
this.initCanvas();
},
beforeDestroy() {
this.stopDrawLoop();
},
methods: {
initCanvas() {
this.dpr = uni.getSystemInfoSync().pixelRatio || 1;
uni
.createSelectorQuery()
.in(this)
.select(`#${'signCanvas'}`)
.boundingClientRect((rect) => {
if (rect) {
this.canvasRect = rect;
this.canvasWidth = rect.width * this.dpr;
this.canvasHeight = rect.height * this.dpr;
}
this.ctx = uni.createCanvasContext('signCanvas', this);
this.startDrawLoop();
})
.exec();
},
getTouchPoint(e) {
const touch = e.touches ? e.touches[0] : e;
const x = touch.x - this.canvasRect.left;
const y = touch.y - this.canvasRect.top;
return { x, y };
},
handleTouchStart(e) {
this.isDrawing = true;
this.currentLine = [];
this.lines.push(this.currentLine);
this.undoneLines = [];
this.lastPoint = this.getTouchPoint(e);
},
handleTouchMove(e) {
if (!this.isDrawing) return;
const point = this.getTouchPoint(e);
this.currentLine.push({
startX: this.lastPoint.x,
startY: this.lastPoint.y,
endX: point.x,
endY: point.y,
color: this.color,
width: this.penWidth,
});
this.lastPoint = point;
},
handleTouchEnd() {
this.isDrawing = false;
this.currentLine = null;
this.drawOnce();
},
drawLine(line) {
this.ctx.beginPath();
this.ctx.moveTo(line.startX, line.startY);
this.ctx.lineTo(line.endX, line.endY);
this.ctx.setStrokeStyle(line.color);
this.ctx.setLineWidth(line.width || 2);
this.ctx.stroke();
},
drawLines() {
if (!this.ctx) return;
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
for (const lineGroup of this.lines) {
for (const line of lineGroup) {
this.drawLine(line);
}
}
this.ctx.draw(false);
this.animationFrameId = setTimeout(() => {
this.drawLines();
}, 16);
},
drawOnce() {
if (!this.ctx) return;
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
for (const lineGroup of this.lines) {
for (const line of lineGroup) {
this.drawLine(line);
}
}
this.ctx.draw(true);
},
startDrawLoop() {
if (this.animationFrameId === null) {
this.drawLines();
}
},
stopDrawLoop() {
if (this.animationFrameId !== null) {
clearTimeout(this.animationFrameId);
this.animationFrameId = null;
}
},
clearCanvas() {
this.lines = [];
this.undoneLines = [];
this.currentLine = null;
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.draw(true);
},
undo() {
if (this.lines.length > 0) {
this.undoneLines.push(this.lines.pop());
this.drawOnce();
}
},
redo() {
if (this.undoneLines.length > 0) {
this.lines.push(this.undoneLines.pop());
this.drawOnce();
}
},
saveImage() {
uni.canvasToTempFilePath(
{
canvasId: 'signCanvas',
success: (res) => {
uni.showToast({ title: "保存成功", icon: "success" });
console.log("保存路径:", res.tempFilePath);
},
fail: (err) => {
uni.showToast({ title: "保存失败", icon: "error" });
console.error(err);
},
},
this
);
},
exportBase64() {
uni.canvasToTempFilePath(
{
canvasId: 'signCanvas',
success: (res) => {
if (uni.getFileSystemManager) {
uni.getFileSystemManager().readFile({
filePath: res.tempFilePath,
encoding: "base64",
success: (fileRes) => {
const base64Data = "data:image/png;base64," + fileRes.data;
uni.setClipboardData({ data: base64Data });
uni.showToast({ title: "Base64已复制", icon: "none" });
console.log(base64Data);
},
});
} else {
uni.showToast({
title: "当前平台不支持导出Base64",
icon: "none",
});
}
},
fail: (err) => {
console.error("导出Base64失败", err);
},
},
this
);
},
},
};
</script>
<style scoped>
.container {
width: 100%;
min-height: 400px;
position: relative;
}
.canvas_container {
width: 100%;
height: 400px;
}
.canvas {
width: 100%;
height: 800rpx;
background: #fff;
border: 1px solid #ddd;
}
.controls {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.color-picker {
width: 40px;
height: 40px;
border: none;
cursor: pointer;
}
.width-control button {
margin-right: 6px;
padding: 4px 8px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
outline: none;
}
.width-control button.active {
border-color: #007aff;
color: #007aff;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
如果有需要有定制化的需要,直接更改 css 即可,本次的只是初步实现的版本