备货单 加 审批流程
This commit is contained in:
@@ -36,11 +36,11 @@
|
||||
<div class="form">
|
||||
<!-- 登录方式切换 tab -->
|
||||
<div class="login-type-tabs">
|
||||
<!-- <span
|
||||
<span
|
||||
:class="{ active: activeLoginType === 'password' }"
|
||||
@click="activeLoginType = 'password'"
|
||||
>账号密码登录</span
|
||||
> -->
|
||||
>
|
||||
<span
|
||||
:class="{ active: activeLoginType === 'qrcode' }"
|
||||
@click="activeLoginType = 'qrcode'"
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user