uniapp中需要使用签名框该如何做,自封装签名框

46 阅读2分钟

下面是自封装适用于各个平台的组件代码

<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 即可,本次的只是初步实现的版本