Files
apple 079076a70e miao33: 从 main 同步 single_uniapp22miao,dart-sass 兼容修复,DEPLOY.md 更新
- 从 main 获取 single_uniapp22miao 子项目
- dart-sass: /deep/ -> ::v-deep,calc 运算符加空格
- DEPLOY.md 采用 shccd159 版本(4 子项目架构说明)

Made-with: Cursor
2026-03-16 11:16:42 +08:00

500 lines
14 KiB
Vue
Raw Permalink 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>
<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>