Files
art-design/src/views/auth/signature/mobile.vue
T
2025-09-08 19:09:55 +08:00

238 lines
6.7 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.
<template>
<div class="signature-page" :class="{ landscape }">
<div class="header">电子签名</div>
<div class="canvas-wrap">
<canvas
ref="canvasRef"
class="canvas"
@touchstart.prevent="onStart"
@touchmove.prevent="onMove"
@touchend.prevent="onEnd"
@mousedown.prevent="onMouseDown"
@mousemove.prevent="onMouseMove"
@mouseup.prevent="onMouseUp"
></canvas>
</div>
<div class="toolbar">
<el-button @click="clearCanvas">清空</el-button>
<el-button @click="toggleLandscape">横屏</el-button>
<el-button type="primary" :loading="submitting" @click="saveSignature">保存</el-button>
</div>
<div class="tip">请在画布上手写签名,然后点击保存</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import { CrmService } from '@/api/crmApi'
import { useUserStore } from '@/store/modules/user'
const route = useRoute()
const userId = route.query.userId as string | undefined
const userStore = useUserStore()
// 优先使用URL传入的token,否则取当前应用内的token
const urlToken = route.query.token as string | undefined
const effectiveToken = urlToken || userStore.accessToken
const { uploadHeaders } = useUploadHeaders()
const canvasRef = ref<HTMLCanvasElement | null>(null)
const ctx = ref<CanvasRenderingContext2D | null>(null)
const drawing = ref(false)
const submitting = ref(false)
const landscape = ref(false)
const resizeCanvas = () => {
const canvas = canvasRef.value
if (!canvas) return
const dpr = window.devicePixelRatio || 1
let width = Math.min(window.innerWidth - 32, 800)
let height = Math.round(width * 0.5)
if (landscape.value && window.innerHeight > 0) {
// 横屏时尽量占满高度
const h = Math.min(window.innerHeight - 120, 500)
height = h
width = Math.round(h * 2)
}
canvas.style.width = width + 'px'
canvas.style.height = height + 'px'
canvas.width = Math.floor(width * dpr)
canvas.height = Math.floor(height * dpr)
const c = canvas.getContext('2d')!
c.scale(dpr, dpr)
c.lineWidth = 3
c.lineCap = 'round'
c.lineJoin = 'round'
c.strokeStyle = '#111'
ctx.value = c
// 背景白色
c.fillStyle = '#fff'
c.fillRect(0, 0, width, height)
}
onMounted(() => {
resizeCanvas()
window.addEventListener('resize', resizeCanvas)
})
const getPoint = (e: TouchEvent | MouseEvent) => {
const canvas = canvasRef.value!
const rect = canvas.getBoundingClientRect()
let clientX = 0
let clientY = 0
if (e instanceof TouchEvent) {
const t = e.touches[0] || e.changedTouches[0]
clientX = t.clientX
clientY = t.clientY
} else {
clientX = (e as MouseEvent).clientX
clientY = (e as MouseEvent).clientY
}
return { x: clientX - rect.left, y: clientY - rect.top }
}
const onStart = (e: TouchEvent) => {
drawing.value = true
const p = getPoint(e)
ctx.value!.beginPath()
ctx.value!.moveTo(p.x, p.y)
}
const onMove = (e: TouchEvent) => {
if (!drawing.value) return
const p = getPoint(e)
ctx.value!.lineTo(p.x, p.y)
ctx.value!.stroke()
}
const onEnd = () => {
drawing.value = false
}
const onMouseDown = (e: MouseEvent) => onStart(e as any)
const onMouseMove = (e: MouseEvent) => onMove(e as any)
const onMouseUp = () => onEnd()
const clearCanvas = () => {
resizeCanvas()
}
const dataURLToFile = (dataURL: string, filename: string) => {
const arr = dataURL.split(',')
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png'
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 })
}
const uploadUrl = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=user_signature`
const requestFullscreen = async (el: HTMLElement) => {
const anyEl = el as any
if (anyEl.requestFullscreen) return anyEl.requestFullscreen()
if (anyEl.webkitRequestFullscreen) return anyEl.webkitRequestFullscreen()
if (anyEl.msRequestFullscreen) return anyEl.msRequestFullscreen()
}
const lockOrientationLandscape = async () => {
const anyScr = screen as any
if (anyScr.orientation && anyScr.orientation.lock) {
try {
await anyScr.orientation.lock('landscape')
} catch {
// ignore
}
}
}
const toggleLandscape = async () => {
landscape.value = !landscape.value
if (landscape.value) {
await requestFullscreen(document.documentElement)
await lockOrientationLandscape()
}
setTimeout(resizeCanvas, 200)
}
const saveSignature = async () => {
if (!userId) {
ElMessage.error('缺少用户信息')
return
}
try {
submitting.value = true
const canvas = canvasRef.value!
const dataUrl = canvas.toDataURL('image/png')
const file = dataURLToFile(dataUrl, `signature_${userId}_${Date.now()}.png`)
const form = new FormData()
form.append('file', file)
const headers: Record<string, string> = { ...(uploadHeaders.value || {}) }
if (effectiveToken) {
headers['Authorization'] = `Bearer ${effectiveToken}`
}
const res = await fetch(uploadUrl, {
method: 'POST',
headers,
body: form
})
const json = await res.json()
if (json.code !== 200) throw new Error(json.message || '上传失败')
// 将签名保存到个人信息(假设 crmuseredit 接受 signature 和 signature_pic_full 字段)
await CrmService.crmuseredit({
id: userId,
signature: json.data.file_name,
signature_pic_full: json.data.file_url
})
ElMessage.success('签名已保存')
// 完成后可提示返回或自动关闭
setTimeout(() => {
if (window.history.length > 1) window.history.back()
}, 800)
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.signature-page {
padding: 16px;
}
.header {
text-align: center;
font-weight: 600;
font-size: 18px;
margin-bottom: 12px;
}
.canvas-wrap {
display: flex;
justify-content: center;
}
.canvas {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 6px;
touch-action: none;
}
.toolbar {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 12px;
}
.tip {
text-align: center;
color: #909399;
margin-top: 8px;
font-size: 12px;
}
</style>