|
@@ -0,0 +1,259 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="signature-body">
|
|
|
|
|
+ <!-- 左侧操作栏 -->
|
|
|
|
|
+ <view class="sidebar">
|
|
|
|
|
+ <view class="btns">
|
|
|
|
|
+ <view class="button" @click="handleClear">重签</view>
|
|
|
|
|
+ <view class="button active" @click="handleSubmit">完成签名</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 签名画布 -->
|
|
|
|
|
+ <view class="canvas">
|
|
|
|
|
+ <canvas
|
|
|
|
|
+ id="signature-canvas"
|
|
|
|
|
+ canvas-id="signature-canvas"
|
|
|
|
|
+ disable-scroll
|
|
|
|
|
+ @touchstart="onTouchStart"
|
|
|
|
|
+ @touchmove="onTouchMove"
|
|
|
|
|
+ @touchend="onTouchEnd"
|
|
|
|
|
+ />
|
|
|
|
|
+ <text class="signature-text">签 名 区</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 隐藏旋转画布 -->
|
|
|
|
|
+ <canvas
|
|
|
|
|
+ class="hidden-canvas"
|
|
|
|
|
+ id="signature-rotate-canvas"
|
|
|
|
|
+ canvas-id="signature-rotate-canvas"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { getCurrentInstance, nextTick, onMounted, ref } from 'vue'
|
|
|
|
|
+
|
|
|
|
|
+const CANVAS_ID = 'signature-canvas'
|
|
|
|
|
+const ROTATE_CANVAS_ID = 'signature-rotate-canvas'
|
|
|
|
|
+
|
|
|
|
|
+const emit = defineEmits<{
|
|
|
|
|
+ (e: 'submit', filePath: string): void
|
|
|
|
|
+ (e: 'clear'): void
|
|
|
|
|
+ (e: 'error', err: unknown): void
|
|
|
|
|
+}>()
|
|
|
|
|
+
|
|
|
|
|
+const instance = getCurrentInstance()
|
|
|
|
|
+
|
|
|
|
|
+const ctx = ref<UniApp.CanvasContext | null>(null)
|
|
|
|
|
+const canvasWidth = ref(0)
|
|
|
|
|
+const canvasHeight = ref(0)
|
|
|
|
|
+
|
|
|
|
|
+const drawing = ref(false)
|
|
|
|
|
+const lastPoint = ref<{ x: number; y: number } | null>(null)
|
|
|
|
|
+
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+
|
|
|
|
|
+ const sys = uni.getSystemInfoSync()
|
|
|
|
|
+ canvasWidth.value = sys.windowWidth
|
|
|
|
|
+ canvasHeight.value = sys.windowHeight
|
|
|
|
|
+
|
|
|
|
|
+ ctx.value = uni.createCanvasContext(CANVAS_ID, instance?.proxy as any)
|
|
|
|
|
+
|
|
|
|
|
+ drawBackground()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const drawBackground = () => {
|
|
|
|
|
+ if (!ctx.value) return
|
|
|
|
|
+
|
|
|
|
|
+ ctx.value.setFillStyle('#ffffff')
|
|
|
|
|
+ ctx.value.fillRect(0, 0, canvasWidth.value, canvasHeight.value)
|
|
|
|
|
+
|
|
|
|
|
+ ctx.value.save()
|
|
|
|
|
+ ctx.value.translate(canvasWidth.value / 2, canvasHeight.value / 2)
|
|
|
|
|
+ ctx.value.rotate(Math.PI / 2)
|
|
|
|
|
+ ctx.value.setFontSize(60)
|
|
|
|
|
+ ctx.value.setFillStyle('rgba(0,0,0,0.06)')
|
|
|
|
|
+ ctx.value.setTextAlign('center')
|
|
|
|
|
+ ctx.value.fillText('签 名 区', 0, 0)
|
|
|
|
|
+ ctx.value.restore()
|
|
|
|
|
+
|
|
|
|
|
+ ctx.value.draw()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onTouchStart = (e: UniApp.TouchEvent) => {
|
|
|
|
|
+ const touch = e.touches?.[0]
|
|
|
|
|
+ if (!touch || !ctx.value) return
|
|
|
|
|
+
|
|
|
|
|
+ drawing.value = true
|
|
|
|
|
+ lastPoint.value = { x: touch.x, y: touch.y }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onTouchMove = (e: UniApp.TouchEvent) => {
|
|
|
|
|
+ if (!drawing.value || !ctx.value || !lastPoint.value) return
|
|
|
|
|
+
|
|
|
|
|
+ const touch = e.touches?.[0]
|
|
|
|
|
+ if (!touch) return
|
|
|
|
|
+
|
|
|
|
|
+ const current = { x: touch.x, y: touch.y }
|
|
|
|
|
+
|
|
|
|
|
+ ctx.value.beginPath()
|
|
|
|
|
+ ctx.value.setLineCap('round')
|
|
|
|
|
+ ctx.value.setStrokeStyle('#000000')
|
|
|
|
|
+ ctx.value.setLineWidth(4)
|
|
|
|
|
+ ctx.value.moveTo(lastPoint.value.x, lastPoint.value.y)
|
|
|
|
|
+ ctx.value.lineTo(current.x, current.y)
|
|
|
|
|
+ ctx.value.stroke()
|
|
|
|
|
+ ctx.value.draw(true)
|
|
|
|
|
+
|
|
|
|
|
+ lastPoint.value = current
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onTouchEnd = () => {
|
|
|
|
|
+ drawing.value = false
|
|
|
|
|
+ lastPoint.value = null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleClear = () => {
|
|
|
|
|
+ if (!ctx.value) return
|
|
|
|
|
+
|
|
|
|
|
+ ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
|
|
|
|
|
+ drawBackground()
|
|
|
|
|
+ emit('clear')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleSubmit = () => {
|
|
|
|
|
+ uni.showLoading({ title: '生成签名中' })
|
|
|
|
|
+
|
|
|
|
|
+ uni.canvasToTempFilePath(
|
|
|
|
|
+ {
|
|
|
|
|
+ canvasId: CANVAS_ID,
|
|
|
|
|
+ fileType: 'png',
|
|
|
|
|
+ quality: 1,
|
|
|
|
|
+ success(res) {
|
|
|
|
|
+ rotateImage(res.tempFilePath)
|
|
|
|
|
+ },
|
|
|
|
|
+ fail(err) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ emit('error', err)
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ instance?.proxy as any
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const rotateImage = (src: string) => {
|
|
|
|
|
+ uni.getImageInfo({
|
|
|
|
|
+ src,
|
|
|
|
|
+ success(info) {
|
|
|
|
|
+ const rotateCtx = uni.createCanvasContext(ROTATE_CANVAS_ID, instance?.proxy as any)
|
|
|
|
|
+
|
|
|
|
|
+ const targetHeight = 300
|
|
|
|
|
+ const ratio = info.height / info.width
|
|
|
|
|
+ const targetWidth = targetHeight / ratio
|
|
|
|
|
+
|
|
|
|
|
+ rotateCtx.translate(targetHeight / 2, targetWidth / 2)
|
|
|
|
|
+ rotateCtx.rotate((270 * Math.PI) / 180)
|
|
|
|
|
+ rotateCtx.drawImage(src, -targetWidth / 2, -targetHeight / 2, targetWidth, targetHeight)
|
|
|
|
|
+
|
|
|
|
|
+ rotateCtx.draw(false, () => {
|
|
|
|
|
+ uni.canvasToTempFilePath(
|
|
|
|
|
+ {
|
|
|
|
|
+ canvasId: ROTATE_CANVAS_ID,
|
|
|
|
|
+ width: targetHeight,
|
|
|
|
|
+ height: targetWidth,
|
|
|
|
|
+ fileType: 'png',
|
|
|
|
|
+ quality: 1,
|
|
|
|
|
+ success(res) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ emit('submit', res.tempFilePath)
|
|
|
|
|
+ },
|
|
|
|
|
+ fail(err) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ emit('error', err)
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ instance?.proxy as any
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ fail(err) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ emit('error', err)
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.signature-body {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+ background: #ffffff;
|
|
|
|
|
+
|
|
|
|
|
+ .sidebar {
|
|
|
|
|
+ width: 150rpx;
|
|
|
|
|
+ background-color: #e5f1fe;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+
|
|
|
|
|
+ .btns {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 100rpx;
|
|
|
|
|
+ transform: rotate(90deg);
|
|
|
|
|
+
|
|
|
|
|
+ .button {
|
|
|
|
|
+ width: 250rpx;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+ line-height: 80rpx;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ border-radius: 6rpx;
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ color: #007bff;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .button.active {
|
|
|
|
|
+ background-color: #4b9ef2;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .canvas {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ canvas {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .signature-text {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transform: rotate(90deg);
|
|
|
|
|
+ font-size: 80rpx;
|
|
|
|
|
+ color: rgba(208, 216, 242, 0.3);
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .hidden-canvas {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ left: -9999px;
|
|
|
|
|
+ top: -9999px;
|
|
|
|
|
+ width: 1px;
|
|
|
|
|
+ height: 1px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|