vue3实现web端和小程序端个人签名

发布于:2025-07-18 ⋅ 阅读:(16) ⋅ 点赞:(0)
<template>
  <el-dialog
    v-model="visible"
    width="900"
    title="签名"
    append-to-body
    @close="closeListDialog"
  >
    <div class="signature-container" pb-10px>
      <!-- 签名区域 -->
      <div class="signature-content">
        <!-- 画布区域 -->
        <div class="canvas-wrapper">
          <canvas
            ref="canvasRef"
            :width="canvasWidth"
            :height="canvasHeight"
            class="signature-canvas"
          ></canvas>
          <div class="signature-prompt" v-if="!hasSignature">请在此处签名</div>
        </div>

        <!-- <div v-if="signatureImage" style="margin-top: 20px">
        <p>预览(实际背景透明):</p>
        <img :src="signatureImage" />
      </div> -->
      </div>
      <!-- 颜色选择 -->
      <div class="color-palette" items-center h-50px>
        <div
          v-for="color in colors"
          :key="color.value"
          class="color-option !mr-10px"
          :class="{ active: currentColor === color.value }"
          :style="{ backgroundColor: color.value }"
          @click="setColor(color.value)"
        ></div>
        <div flex-1></div>
        <el-button link size="small" class="btn undo" @click="undo">
          撤销
        </el-button>
        <el-button
          link
          size="small"
          my-30px
          class="btn clear"
          @click="clearCanvas"
        >
          清除
        </el-button>
        <el-button
          size="small"
          type="danger"
          class="btn"
          @click="closeListDialog"
        >
          取消
        </el-button>
        <el-button size="small" class="btn confirm" @click="saveSignature">
          完成
        </el-button>
      </div>
    </div>
  </el-dialog>
</template>

<script setup lang="ts">
const emit = defineEmits<{
  (e: "ok", value: any): void;
}>();
const visible = ref(false);

const closeListDialog = () => {
  clearCanvas();
  visible.value = false;
};

const open = () => {
  visible.value = true;
  nextTick(() => {
    initCanvas();
    window.addEventListener("resize", handleResize); // 确保对话框和 Canvas 已渲染
  });
};
defineExpose({
  open,
});
// 画布尺寸
const canvasWidth = ref(850);
const canvasHeight = ref(400);
const canvasRef: any = ref(null);
const signatureImage = ref("");
const hasSignature = ref(false);
const currentColor = ref("#000000");

// 颜色选项
const colors = ref([
  { value: "#000000", name: "黑色" },
  { value: "#FF0000", name: "红色" },
  { value: "#0066FF", name: "蓝色" },
]);

// 绘图状态
let ctx: any = null;
let isDrawing = false;
let lastX = 0;
let lastY = 0;
let drawingHistory: any = [];

const initCanvas = () => {
  const canvas: any = canvasRef.value;
  ctx = canvas.getContext("2d");

  // 设置透明背景
  ctx.fillStyle = "rgba(0, 0, 0, 0)";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // 设置初始画笔样式
  setColor(currentColor.value);

  // 绑定触摸事件
  setupTouchEvents(canvas);
};

// 设置画笔颜色
const setColor = (color: any) => {
  currentColor.value = color;
  ctx.strokeStyle = color;
  ctx.lineWidth = 6;
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
};

// 绑定触摸事件
const setupTouchEvents = (canvas: any) => {
  canvas.addEventListener("touchstart", handleTouchStart);
  canvas.addEventListener("touchmove", handleTouchMove);
  canvas.addEventListener("touchend", handleTouchEnd);
  canvas.addEventListener("mousedown", handleMouseDown);
  canvas.addEventListener("mousemove", handleMouseMove);
  canvas.addEventListener("mouseup", handleMouseUp);
};

// 触摸事件处理
const handleTouchStart = (e: any) => {
  e.preventDefault();
  const touch = getTouchPos(e);
  startDrawing(touch.x, touch.y);
};

const handleTouchMove = (e: any) => {
  e.preventDefault();
  const touch = getTouchPos(e);
  draw(touch.x, touch.y);
};

const handleTouchEnd = () => {
  endDrawing();
};

// 鼠标事件处理(用于开发调试)
const handleMouseDown = (e: any) => {
  const pos = getMousePos(e);
  startDrawing(pos.x, pos.y);
};

const handleMouseMove = (e: any) => {
  if (!isDrawing) return;
  const pos = getMousePos(e);
  draw(pos.x, pos.y);
};

const handleMouseUp = () => {
  endDrawing();
};

// 开始绘制
const startDrawing = (x: any, y: any) => {
  isDrawing = true;
  lastX = x;
  lastY = y;
  ctx.beginPath();
  ctx.moveTo(x, y);
  saveDrawingState();
};

// 绘制过程
const draw = (x: any, y: any) => {
  if (!isDrawing) return;

  ctx.lineTo(x, y);
  ctx.stroke();

  lastX = x;
  lastY = y;
  hasSignature.value = true;
};

// 结束绘制
const endDrawing = () => {
  isDrawing = false;
};

// 保存绘图状态
const saveDrawingState = () => {
  const canvas = canvasRef.value;
  drawingHistory.push(canvas.toDataURL());
  if (drawingHistory.length > 20) {
    drawingHistory.shift();
  }
};

// 撤销操作
const undo = () => {
  if (drawingHistory.length > 0) {
    const lastState = drawingHistory.pop();
    const img = new Image();
    img.onload = () => {
      ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
      ctx.drawImage(img, 0, 0);
      hasSignature.value = drawingHistory.length > 0;
    };
    img.src = lastState;
  }
};

// 清除画布
const clearCanvas = () => {
  ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
  drawingHistory = [];
  hasSignature.value = false;
};

// 保存签名
const saveSignature = () => {
  if (!hasSignature.value) {
    alert("请先签名");
    return;
  }

  // 创建临时Canvas确保导出质量
  const tempCanvas = document.createElement("canvas");
  tempCanvas.width = canvasRef.value.width;
  tempCanvas.height = canvasRef.value.height;
  const tempCtx: any = tempCanvas.getContext("2d");

  // 绘制签名内容
  tempCtx.drawImage(canvasRef.value, 0, 0);

  // 导出为PNG
  signatureImage.value = tempCanvas.toDataURL("image/png");

  // 这里可以触发父组件事件或上传到服务器
  console.log("签名图片:", signatureImage.value);
  emit("ok", signatureImage.value);
  closeListDialog();
};

// 响应式调整
const handleResize = () => {
  canvasWidth.value = window.innerWidth * 0.8;
  canvasHeight.value = window.innerHeight * 0.6;
};

// 获取触摸位置(改进版)
const getTouchPos = (e: any) => {
  const canvas: any = canvasRef.value;
  const rect = canvas.getBoundingClientRect();
  const touch = e.touches[0] || e.changedTouches[0];
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;

  return {
    x: (touch.clientX - rect.left) * scaleX,
    y: (touch.clientY - rect.top) * scaleY,
  };
};

// 获取鼠标位置(改进版)
const getMousePos = (e: any) => {
  const canvas: any = canvasRef.value;
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;

  return {
    x: (e.clientX - rect.left) * scaleX,
    y: (e.clientY - rect.top) * scaleY,
  };
};
</script>

<style scoped>
.signature-container {
  position: relative;
  /* width: 100vw;
  height: 100vh; */
  background-color: #f5f5f5;
  display: flex;
  flex-direction: column;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, sans-serif;
}

.status-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 5px 15px;
  background-color: white;
  font-size: 14px;
}

.signature-content {
  display: flex;
  flex: 1;
  padding: 10px;
  flex-direction: column;
}

.color-palette {
  display: flex;
  flex-direction: row;
  align-items: center;
  background-color: white;
  border-radius: 10px;
  margin-inline: 10px;
  padding-inline: 10px;
}

.color-option {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  margin: 8px 0;
  cursor: pointer;
  border: 2px solid #eee;
}

.color-option.active {
  border-color: #0066ff;
}

.canvas-wrapper {
  /* flex: 1;
  position: relative;
  background-color: white;
  border-radius: 10px;
  overflow: hidden; */
}

.signature-canvas {
  display: block;
  background-color: white;
}

.signature-prompt {
  position: absolute;
  top: calc(50% - 18px);
  left: calc(50% - 50px);
  color: #999;
  font-size: 16px;
}

.action-buttons {
  display: flex;
  justify-content: space-around;
  padding: 15px;
  background-color: white;
}

.btn {
  border: none;
  border-radius: 20px;
  font-size: 14px;
  cursor: pointer;
}

.undo {
  color: #333;
}

.clear {
  color: #333;
}

.confirm {
  background-color: #0066ff;
  color: white;
}

.address-bar {
  padding: 8px 15px;
  background-color: white;
  text-align: center;
  font-size: 12px;
  color: #0066ff;
  border-top: 1px solid #eee;
}
</style>


使用:

```javascript
<signature ref="signatureRef" @ok="getsignature" />

import signature from "./signature.vue"

const signatureRef: any = ref(null);
const addSignature = () => {
  signatureRef.value?.open();
};

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到