500 lines
14 KiB
Vue
500 lines
14 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="sign-page" :class="{ 'landscape-mode': isRotated }">
|
|||
|
|
<view class="top-right" @click="goBack"><text class="back-text">返回</text></view>
|
|||
|
|
<canvas
|
|||
|
|
canvas-id="signCanvas"
|
|||
|
|
class="sign-canvas"
|
|||
|
|
@touchstart="touchStart"
|
|||
|
|
@touchmove="touchMove"
|
|||
|
|
@touchend="touchEnd"
|
|||
|
|
@mousedown="mouseStart"
|
|||
|
|
@mousemove="mouseMove"
|
|||
|
|
@mouseup="mouseEnd"
|
|||
|
|
@mouseleave="mouseEnd"
|
|||
|
|
:disable-scroll="true"
|
|||
|
|
></canvas>
|
|||
|
|
<view class="bottom-actions">
|
|||
|
|
<view class="btn primary" @click="clearCanvas"><text class="btn-text">重试</text></view>
|
|||
|
|
<view class="btn primary" @click="saveSign"><text class="btn-text">我已同意并确认签名</text></view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { uploadUserImage } from '@/api/miao.js';
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
ctx: null,
|
|||
|
|
canvasWidth: 0,
|
|||
|
|
canvasHeight: 0,
|
|||
|
|
windowWidth: 0,
|
|||
|
|
windowHeight: 0,
|
|||
|
|
isRotated: false,
|
|||
|
|
isDrawing: false,
|
|||
|
|
hasSignature: false,
|
|||
|
|
uploading: false,
|
|||
|
|
canvasRect: null,
|
|||
|
|
userId: '',
|
|||
|
|
dpr: 1,
|
|||
|
|
offsetFix: { x: 0, y: 0 },
|
|||
|
|
points: [],
|
|||
|
|
rafId: 0,
|
|||
|
|
lastPointTime: 0
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onLoad(options) {
|
|||
|
|
// Check orientation
|
|||
|
|
try {
|
|||
|
|
const sys = uni.getSystemInfoSync();
|
|||
|
|
this.windowWidth = sys.windowWidth;
|
|||
|
|
this.windowHeight = sys.windowHeight;
|
|||
|
|
if (this.windowHeight > this.windowWidth) {
|
|||
|
|
this.isRotated = true;
|
|||
|
|
}
|
|||
|
|
} catch(e) {}
|
|||
|
|
|
|||
|
|
this.initCanvas();
|
|||
|
|
// #ifdef H5
|
|||
|
|
const getUserIdFromUrl = () => {
|
|||
|
|
let id = ''
|
|||
|
|
const s = window.location.search || ''
|
|||
|
|
if (s) {
|
|||
|
|
const p = new URLSearchParams(s)
|
|||
|
|
id = p.get('user_id') || p.get('userId') || ''
|
|||
|
|
}
|
|||
|
|
if (!id && window.location.hash) {
|
|||
|
|
const hash = window.location.hash
|
|||
|
|
const qIndex = hash.indexOf('?')
|
|||
|
|
if (qIndex !== -1) {
|
|||
|
|
const q = hash.substring(qIndex + 1)
|
|||
|
|
const hp = new URLSearchParams(q)
|
|||
|
|
id = hp.get('user_id') || hp.get('userId') || ''
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (!id) {
|
|||
|
|
const href = window.location.href || ''
|
|||
|
|
const m = href.match(/[?&]user_id=([^&#]+)/)
|
|||
|
|
if (m) id = decodeURIComponent(m[1])
|
|||
|
|
}
|
|||
|
|
return id
|
|||
|
|
}
|
|||
|
|
this.userId = (options && options.user_id) ? options.user_id : getUserIdFromUrl()
|
|||
|
|
console.log('===sign page -> userId===', this.userId)
|
|||
|
|
if (this.userId) {
|
|||
|
|
try { localStorage.setItem('user_id', this.userId) } catch(e) {}
|
|||
|
|
if (typeof uni !== 'undefined' && uni.setStorageSync) uni.setStorageSync('user_id', this.userId)
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
},
|
|||
|
|
onResize() {
|
|||
|
|
const sys = uni.getSystemInfoSync();
|
|||
|
|
this.windowWidth = sys.windowWidth;
|
|||
|
|
this.windowHeight = sys.windowHeight;
|
|||
|
|
},
|
|||
|
|
onReady() {
|
|||
|
|
const q = uni.createSelectorQuery().in(this)
|
|||
|
|
q.select('.sign-canvas').boundingClientRect((rect) => {
|
|||
|
|
this.canvasRect = rect
|
|||
|
|
}).exec()
|
|||
|
|
try {
|
|||
|
|
const sys = uni.getSystemInfoSync()
|
|||
|
|
this.dpr = sys.pixelRatio || 1
|
|||
|
|
} catch(e) {}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
// 初始化画布
|
|||
|
|
initCanvas() {
|
|||
|
|
const that = this;
|
|||
|
|
// 获取系统信息来设置画布大小
|
|||
|
|
uni.getSystemInfo({
|
|||
|
|
success: (res) => {
|
|||
|
|
that.windowWidth = res.windowWidth;
|
|||
|
|
that.windowHeight = res.windowHeight;
|
|||
|
|
|
|||
|
|
if (that.isRotated) {
|
|||
|
|
that.canvasWidth = res.windowHeight;
|
|||
|
|
that.canvasHeight = res.windowWidth;
|
|||
|
|
} else {
|
|||
|
|
that.canvasWidth = res.windowWidth;
|
|||
|
|
that.canvasHeight = res.windowHeight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建绘图上下文
|
|||
|
|
that.ctx = uni.createCanvasContext('signCanvas', that);
|
|||
|
|
that.ctx.setStrokeStyle('#FF0000');
|
|||
|
|
that.ctx.setLineWidth(2);
|
|||
|
|
that.ctx.setLineCap('round');
|
|||
|
|
that.ctx.setLineJoin('round');
|
|||
|
|
|
|||
|
|
// 绘制白色背景
|
|||
|
|
that.ctx.setFillStyle('#FFFFFF');
|
|||
|
|
that.ctx.fillRect(0, 0, that.canvasWidth, that.canvasHeight);
|
|||
|
|
that.ctx.save();
|
|||
|
|
that.ctx.setFillStyle('rgba(0,0,0,0.06)');
|
|||
|
|
that.ctx.setFontSize(56);
|
|||
|
|
if (that.ctx.setTextAlign) that.ctx.setTextAlign('center');
|
|||
|
|
if (that.ctx.setTextBaseline) that.ctx.setTextBaseline('middle');
|
|||
|
|
const cx = that.canvasWidth / 2;
|
|||
|
|
const cy = that.canvasHeight / 2;
|
|||
|
|
that.ctx.translate(cx, cy);
|
|||
|
|
// that.ctx.rotate(-Math.PI / 2); // Remove rotation as we are now in landscape mode
|
|||
|
|
that.ctx.fillText('签字区域', 0, -40);
|
|||
|
|
that.ctx.fillText('在该白板区域签名', 0, 40);
|
|||
|
|
that.ctx.restore();
|
|||
|
|
that.ctx.draw();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 触摸开始
|
|||
|
|
touchStart(e) {
|
|||
|
|
this.isDrawing = true;
|
|||
|
|
this.hasSignature = true;
|
|||
|
|
const p = this.normalizePoint(e);
|
|||
|
|
this.points = [p];
|
|||
|
|
this.lastPointTime = p.t;
|
|||
|
|
this.queueDraw();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 触摸移动
|
|||
|
|
touchMove(e) {
|
|||
|
|
if (!this.isDrawing) return;
|
|||
|
|
const p = this.normalizePoint(e);
|
|||
|
|
this.points.push(p);
|
|||
|
|
this.queueDraw();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 触摸结束
|
|||
|
|
touchEnd() {
|
|||
|
|
this.isDrawing = false;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 鼠标开始(PC)
|
|||
|
|
mouseStart(e) {
|
|||
|
|
this.isDrawing = true;
|
|||
|
|
this.hasSignature = true;
|
|||
|
|
const p = this.normalizePoint(e);
|
|||
|
|
this.points = [p];
|
|||
|
|
this.lastPointTime = p.t;
|
|||
|
|
this.queueDraw();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 鼠标移动(PC)
|
|||
|
|
mouseMove(e) {
|
|||
|
|
if (!this.isDrawing) return;
|
|||
|
|
const p = this.normalizePoint(e);
|
|||
|
|
this.points.push(p);
|
|||
|
|
this.queueDraw();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 鼠标结束(PC)
|
|||
|
|
mouseEnd() {
|
|||
|
|
this.isDrawing = false;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 坐标统一抽取
|
|||
|
|
normalizePoint(e) {
|
|||
|
|
const rect = this.canvasRect || { left: 0, top: 0 }
|
|||
|
|
const now = Date.now()
|
|||
|
|
const t = (e && e.touches && e.touches[0]) || (e && e.changedTouches && e.changedTouches[0])
|
|||
|
|
let x = 0, y = 0, pressure = 0.5
|
|||
|
|
|
|||
|
|
let clientX = 0, clientY = 0
|
|||
|
|
|
|||
|
|
if (t) {
|
|||
|
|
clientX = (t.clientX ?? t.pageX ?? t.x ?? 0)
|
|||
|
|
clientY = (t.clientY ?? t.pageY ?? t.y ?? 0)
|
|||
|
|
pressure = typeof t.force === 'number' ? t.force : 0.5
|
|||
|
|
} else {
|
|||
|
|
// PC Mouse
|
|||
|
|
clientX = (e.clientX ?? e.pageX ?? 0)
|
|||
|
|
clientY = (e.clientY ?? e.pageY ?? 0)
|
|||
|
|
pressure = typeof e.pressure === 'number' ? e.pressure : 0.5
|
|||
|
|
|
|||
|
|
// If offsetX is available and we are not rotated (or browser handles it), we could use it.
|
|||
|
|
// But to be consistent with rotation logic, let's use client coordinates and map them.
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.isRotated) {
|
|||
|
|
// Mapping for rotate(90deg) transform-origin 0 0 left 100vw
|
|||
|
|
// Visual X (Canvas X) = ClientY
|
|||
|
|
// Visual Y (Canvas Y) = WindowWidth - ClientX
|
|||
|
|
// Note: this.windowWidth is the Screen Width (which becomes Canvas Height)
|
|||
|
|
|
|||
|
|
const winW = this.windowWidth || uni.getSystemInfoSync().windowWidth;
|
|||
|
|
|
|||
|
|
x = clientY + this.offsetFix.x
|
|||
|
|
y = (winW - clientX) + this.offsetFix.y
|
|||
|
|
} else {
|
|||
|
|
// Normal mode
|
|||
|
|
// For Mouse with offsetX, use it if available to avoid rect issues?
|
|||
|
|
// But the original code mixed clientX and offsetX.
|
|||
|
|
// Let's stick to clientX - rect.left for consistency if rect is valid.
|
|||
|
|
|
|||
|
|
if (!t && (e.offsetX !== undefined)) {
|
|||
|
|
// Trust offsetX for mouse if available (simplifies things)
|
|||
|
|
x = e.offsetX + this.offsetFix.x
|
|||
|
|
y = e.offsetY + this.offsetFix.y
|
|||
|
|
} else {
|
|||
|
|
x = clientX - rect.left + this.offsetFix.x
|
|||
|
|
y = clientY - rect.top + this.offsetFix.y
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { x, y, p: pressure, t: now }
|
|||
|
|
},
|
|||
|
|
widthFor(p0, p1) {
|
|||
|
|
const dt = Math.max(1, p1.t - p0.t)
|
|||
|
|
const dx = p1.x - p0.x
|
|||
|
|
const dy = p1.y - p0.y
|
|||
|
|
const v = Math.sqrt(dx*dx + dy*dy) / dt
|
|||
|
|
const pressure = (p1.p ?? 0.5)
|
|||
|
|
const base = 3
|
|||
|
|
const byPressure = base + pressure * 5
|
|||
|
|
const bySpeed = Math.max(3, 8 - v * 2)
|
|||
|
|
return Math.min(8, Math.max(3, (byPressure + bySpeed) / 2))
|
|||
|
|
},
|
|||
|
|
queueDraw() {
|
|||
|
|
if (this.rafId) return
|
|||
|
|
this.rafId = requestAnimationFrame(this.flushDraw)
|
|||
|
|
},
|
|||
|
|
flushDraw() {
|
|||
|
|
this.rafId = 0
|
|||
|
|
if (this.points.length < 2) return
|
|||
|
|
const pts = this.points
|
|||
|
|
let i = 1
|
|||
|
|
this.ctx.setStrokeStyle('#000000')
|
|||
|
|
this.ctx.setLineCap('round')
|
|||
|
|
this.ctx.setLineJoin('round')
|
|||
|
|
while (i < pts.length) {
|
|||
|
|
const p0 = pts[i-1]
|
|||
|
|
const p1 = pts[i]
|
|||
|
|
this.ctx.beginPath()
|
|||
|
|
this.ctx.setLineWidth(this.widthFor(p0, p1))
|
|||
|
|
this.ctx.moveTo(p0.x, p0.y)
|
|||
|
|
this.ctx.lineTo(p1.x, p1.y)
|
|||
|
|
this.ctx.stroke()
|
|||
|
|
i++
|
|||
|
|
}
|
|||
|
|
this.ctx.draw(true)
|
|||
|
|
this.points = this.points.slice(-1)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 清空画布
|
|||
|
|
clearCanvas() {
|
|||
|
|
if (!this.hasSignature) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '画布已经是空的',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '提示',
|
|||
|
|
content: '确定要清空签名吗?',
|
|||
|
|
success: (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
// 清空画布
|
|||
|
|
this.ctx.setFillStyle('#FFFFFF');
|
|||
|
|
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
|
|||
|
|
this.ctx.save();
|
|||
|
|
this.ctx.setFillStyle('rgba(0,0,0,0.06)');
|
|||
|
|
this.ctx.setFontSize(56);
|
|||
|
|
if (this.ctx.setTextAlign) this.ctx.setTextAlign('center');
|
|||
|
|
if (this.ctx.setTextBaseline) this.ctx.setTextBaseline('middle');
|
|||
|
|
const cx = this.canvasWidth / 2;
|
|||
|
|
const cy = this.canvasHeight / 2;
|
|||
|
|
this.ctx.translate(cx, cy);
|
|||
|
|
// this.ctx.rotate(-Math.PI / 2);
|
|||
|
|
this.ctx.fillText('签字区域', 0, -40);
|
|||
|
|
this.ctx.fillText('在该白板区域签名', 0, 40);
|
|||
|
|
this.ctx.restore();
|
|||
|
|
this.ctx.draw();
|
|||
|
|
this.hasSignature = false;
|
|||
|
|
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '已清空',
|
|||
|
|
icon: 'success',
|
|||
|
|
duration: 1000
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 保存签名并上传
|
|||
|
|
async saveSign() {
|
|||
|
|
if (!this.hasSignature) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请先签名',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.uploading) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.uploading = true;
|
|||
|
|
uni.showLoading({
|
|||
|
|
title: '正在保存...'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 将画布导出为图片
|
|||
|
|
const tempFilePath = await this.canvasToTempFile();
|
|||
|
|
const uploadRes = await uploadUserImage(tempFilePath, this.userId, 'user');
|
|||
|
|
|
|||
|
|
uni.hideLoading();
|
|||
|
|
|
|||
|
|
if (uploadRes.code === 0 || uploadRes.code === 200) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '签名保存成功',
|
|||
|
|
icon: 'success'
|
|||
|
|
});
|
|||
|
|
// 返回
|
|||
|
|
setTimeout(() => {
|
|||
|
|
// window.location.href = 'https://shop.wenjinhui.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
|
|||
|
|
// window.location.href = 'https://anyue.szxingming.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
|
|||
|
|
window.location.href = 'https://xiashengjun.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
|
|||
|
|
// window.location.href = 'https://shop.uj345.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
|
|||
|
|
}, 1000)
|
|||
|
|
// 返回签名信息给上一页面
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const pages = getCurrentPages();
|
|||
|
|
const prevPage = pages[pages.length - 2];
|
|||
|
|
|
|||
|
|
if (prevPage) {
|
|||
|
|
// 可以通过事件或直接设置数据的方式传递签名信息
|
|||
|
|
prevPage.$vm.signatureUrl = uploadRes.data.url;
|
|||
|
|
}
|
|||
|
|
uni.navigateBack();
|
|||
|
|
}, 15000);
|
|||
|
|
} else {
|
|||
|
|
throw new Error(uploadRes.msg || '上传失败');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
uni.hideLoading();
|
|||
|
|
console.error('保存签名失败:', error);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: error.message || '保存失败,请重试',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
} finally {
|
|||
|
|
this.uploading = false;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 将画布转为临时文件
|
|||
|
|
canvasToTempFile() {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
uni.canvasToTempFilePath({
|
|||
|
|
canvasId: 'signCanvas',
|
|||
|
|
fileType: 'png',
|
|||
|
|
quality: 1,
|
|||
|
|
success: (res) => {
|
|||
|
|
resolve(res.tempFilePath);
|
|||
|
|
},
|
|||
|
|
fail: (err) => {
|
|||
|
|
reject(err);
|
|||
|
|
}
|
|||
|
|
}, this);
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 返回上一页
|
|||
|
|
goBack() {
|
|||
|
|
if (this.hasSignature) {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '提示',
|
|||
|
|
content: '签名尚未保存,确定要退出吗?',
|
|||
|
|
success: (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
uni.navigateBack();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
uni.navigateBack();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.sign-page {
|
|||
|
|
position: relative;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
// background-color: #f2f2f2;
|
|||
|
|
overflow: hidden;
|
|||
|
|
|
|||
|
|
&.landscape-mode {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 100vw;
|
|||
|
|
width: 100vh;
|
|||
|
|
height: 100vw;
|
|||
|
|
transform: rotate(90deg);
|
|||
|
|
transform-origin: 0 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.top-right {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 20rpx;
|
|||
|
|
right: 20rpx;
|
|||
|
|
z-index: 100;
|
|||
|
|
background: #fff;
|
|||
|
|
border: 2rpx solid #E0E0E0;
|
|||
|
|
border-radius: 12rpx;
|
|||
|
|
padding: 14rpx 50rpx;
|
|||
|
|
box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.back-text { color: #666; font-size: 26rpx; }
|
|||
|
|
|
|||
|
|
.sign-canvas {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
top: 0;
|
|||
|
|
bottom: 140rpx;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
background-color: #FFFFFF;
|
|||
|
|
box-shadow: 0 0 0 0 transparent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.bottom-actions {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 1px;
|
|||
|
|
height: 120rpx;
|
|||
|
|
background: #f9f9f9;
|
|||
|
|
border-top: 1rpx solid #eee;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-around;
|
|||
|
|
padding: 0 40rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn {
|
|||
|
|
background: #FF6B6B;
|
|||
|
|
border-radius: 16rpx;
|
|||
|
|
padding: 20rpx 60rpx;
|
|||
|
|
box-shadow: 0 8rpx 20rpx rgba(255, 45, 45, 0.25);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn.primary { background: #FF2D2D; }
|
|||
|
|
.btn-text { color: #fff; font-size: 28rpx; }
|
|||
|
|
</style>
|
|||
|
|
|