|
- <template>
- <view class="sign-page">
- <view class="canvas-wrap">
- <view class="hint-text">
- <text>请在此处手写签名</text>
- </view>
- <canvas id="signCanvas" canvas-id="signCanvas" type="2d" class="sign-canvas" disable-scroll
- @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" />
- </view>
- <view class="bottom-btns">
- <view class="btn-clear" @click="handleClear">
- <text>清除</text>
- </view>
- <view class="btn-save" @click="handleSave">
- <text>保存</text>
- </view>
- </view>
- </view>
- </template>
-
- <script setup>
- import { ref, nextTick } from 'vue'
- import { onLoad } from '@dcloudio/uni-app'
- import { upload } from '@/utils/request.js'
-
- let ctx = null
- let canvasEl = null
- let canvasWidth = 0
- let canvasHeight = 0
- const hasDrawn = ref(false)
- const saving = ref(false)
- let lastX = 0
- let lastY = 0
-
- onLoad(async () => {
- await nextTick()
- setTimeout(() => {
- const query = uni.createSelectorQuery()
- query.select('#signCanvas').fields({ node: true, size: true }).exec((res) => {
- if (!res[0]) return
- canvasEl = res[0].node
- const dpr = uni.getSystemInfoSync().pixelRatio
- canvasWidth = res[0].width
- canvasHeight = res[0].height
- canvasEl.width = canvasWidth * dpr
- canvasEl.height = canvasHeight * dpr
- ctx = canvasEl.getContext('2d')
- ctx.scale(dpr, dpr)
- ctx.lineCap = 'round'
- ctx.lineJoin = 'round'
- ctx.lineWidth = 4
- ctx.strokeStyle = '#333'
- })
- }, 300)
- })
-
- const getPos = (e) => {
- const touch = e.touches[0]
- return { x: touch.x, y: touch.y }
- }
-
- const onTouchStart = (e) => {
- if (!ctx) return
- hasDrawn.value = true
- const { x, y } = getPos(e)
- lastX = x
- lastY = y
- ctx.beginPath()
- ctx.moveTo(x, y)
- }
-
- const onTouchMove = (e) => {
- if (!ctx) return
- const { x, y } = getPos(e)
- ctx.beginPath()
- ctx.moveTo(lastX, lastY)
- ctx.lineTo(x, y)
- ctx.stroke()
- lastX = x
- lastY = y
- }
-
- const onTouchEnd = () => {}
-
- const handleClear = () => {
- if (!ctx || !canvasEl) return
- ctx.clearRect(0, 0, canvasWidth, canvasHeight)
- hasDrawn.value = false
- }
-
- const handleSave = () => {
- if (!hasDrawn.value) {
- uni.showToast({ title: '请先签名', icon: 'none' })
- return
- }
- if (saving.value) return
- saving.value = true
- uni.showLoading({ title: '保存中...' })
-
- uni.canvasToTempFilePath({
- canvas: canvasEl,
- fileType: 'png',
- quality: 1,
- success: async (res) => {
- try {
- const dpr = uni.getSystemInfoSync().pixelRatio
- // 创建离屏 canvas 旋转签名(横屏→竖屏,逆时针90度)
- const offscreen = uni.createOffscreenCanvas({
- type: '2d',
- width: canvasHeight * dpr,
- height: canvasWidth * dpr
- })
- const offCtx = offscreen.getContext('2d')
- const img = offscreen.createImage()
- await new Promise((resolve, reject) => {
- img.onload = resolve
- img.onerror = reject
- img.src = res.tempFilePath
- })
- offCtx.translate(0, canvasWidth * dpr)
- offCtx.rotate(-Math.PI / 2)
- offCtx.drawImage(img, 0, 0)
-
- const rotatedRes = await new Promise((resolve, reject) => {
- uni.canvasToTempFilePath({
- canvas: offscreen,
- fileType: 'png',
- quality: 1,
- success: resolve,
- fail: reject
- })
- })
-
- // 上传签名图
- const uploadRes = await upload('/api/mp/upload', {
- filePath: rotatedRes.tempFilePath,
- name: 'file'
- })
- if (!uploadRes.data || !uploadRes.data.url) {
- throw { msg: '上传失败' }
- }
-
- uni.hideLoading()
- // 把签名图 URL 传回 sign 页面
- uni.$emit('signatureResult', { url: uploadRes.data.url })
- uni.navigateBack()
- } catch (err) {
- uni.hideLoading()
- uni.showToast({ title: err.msg || '上传失败,请重试', icon: 'none' })
- } finally {
- saving.value = false
- }
- },
- fail: () => {
- uni.hideLoading()
- uni.showToast({ title: '保存失败', icon: 'none' })
- saving.value = false
- }
- })
- }
- </script>
-
- <style lang="scss" scoped>
- .sign-page {
- display: flex;
- flex-direction: column;
- height: 100vh;
- background: #edf2fc;
- }
-
- .canvas-wrap {
- flex: 1;
- position: relative;
- overflow: hidden;
-
- .hint-text {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%) rotate(90deg);
- pointer-events: none;
- white-space: nowrap;
- z-index: 0;
-
- text {
- font-size: 48rpx;
- color: #c0c8d8;
- font-weight: 300;
- letter-spacing: 16rpx;
- }
- }
-
- .sign-canvas {
- width: 100%;
- height: 100%;
- position: relative;
- z-index: 1;
- }
- }
-
- .bottom-btns {
- display: flex;
- height: calc(100rpx + env(safe-area-inset-bottom));
- flex-shrink: 0;
- padding-bottom: env(safe-area-inset-bottom);
-
- .btn-clear {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #fff;
-
- text {
- writing-mode: vertical-rl;
- transform: rotate(90deg);
- font-size: 32rpx;
- color: #333;
- letter-spacing: 4rpx;
- }
- }
-
- .btn-save {
- flex: 2;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #0e63e3;
-
- text {
- writing-mode: vertical-rl;
- transform: rotate(90deg);
- font-size: 32rpx;
- color: #fff;
- letter-spacing: 4rpx;
- }
- }
- }
- </style>
|