2025-08-27 14:14:41 +08:00

788 lines
19 KiB
Vue

<template>
<view>
<view class="wrapper">
<!-- <view class="handRight">
<view class="handTitle">请签名</view>
</view> -->
<!-- <view class="nav-bar">
<view class="nav-left" @click="handleBack">
<u-icon name="arrow-left" size="44"></u-icon>
<text>返回</text>
</view>
<text class="nav-title">签名</text>
</view> -->
<view class="handCenter">
<canvas class="handWriting" :disable-scroll="true" @touchstart="uploadScaleStart"
@touchmove="uploadScaleMove" @touchend="uploadScaleEnd" canvas-id="handWriting"></canvas>
</view>
<view class="handBtn">
<!-- #ifdef MP-WEIXIN -->
<!-- <image @click="selectColorEvent('black','#1A1A1A')"
:src="selectColor === 'black' ? '/static/other/color_black_selected.png' : '/static/other/color_black.png'"
class="black-select"></image>
<image @click="selectColorEvent('red','#ca262a')"
:src="selectColor === 'red' ? '/static/other/color_red_selected.png' : '/static/other/color_red.png'"
class="red-select"></image> -->
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<div class="color_pic" :style="{background:lineColor}" @click="showPickerColor=true"></div>
<!-- #endif -->
<button @click="clear" class="delBtn">清空</button>
<!-- <button @click="saveCanvasAsImg" class="saveBtn">保存</button> -->
<!-- <button @click="previewCanvasImg" class="previewBtn">预览</button> -->
<button @click="undo" class="undoBtn">撤销</button>
<button class="subBtn" @click="postApply">完成</button>
</view>
</view>
<pickerColor :isShow="showPickerColor" :bottom="0" @callback='getPickerColor' />
<!-- <u-popup :show="readShow" mode="bottom" :round="10">
<instructionVue @change='readChange' />
<u-button type="primary" text="我已知晓" :disabled="hasReaded" @click="postApply"></u-button>
</u-popup> -->
</view>
</template>
<script>
import {
post
} from "../../utils/request";
import pickerColor from "./pickerColor.vue"
import {
IMAGE_BASE_URL,
BASE_URL
} from '@/utils/config';
import {
downloadPdfFiles
} from '@/utils/download.js'
import instructionVue from '../../components/instruction.vue';
export default {
components: {
pickerColor,
instructionVue
},
data() {
return {
readShow: false,
hasReaded: true,
showPickerColor: false,
ctx: '',
canvasWidth: 0,
canvasHeight: 0,
selectColor: 'black',
lineColor: '#1A1A1A',
points: [],
historyList: [],
canAddHistory: true,
getImagePath: () => {
return new Promise((resolve) => {
uni.canvasToTempFilePath({
canvasId: 'handWriting',
fileType: 'png',
quality: 1, //图片质量
success: res => resolve(res.tempFilePath),
})
})
},
toDataURL: void 0,
requestAnimationFrame: void 0,
applyInfo: null,
isSelfStudy: true,
};
},
props: { //可用于修改的参数放在props里 也可单独放在外面做成组件调用 传值
minSpeed: { //画笔最小速度
type: Number,
default: 1.5
},
minWidth: { //线条最小粗度
type: Number,
default: 3,
},
maxWidth: { //线条最大粗度
type: Number,
default: 10
},
openSmooth: { //开启平滑线条(笔锋)
type: Boolean,
default: true
},
maxHistoryLength: { //历史最大长度
type: Number,
default: 20
},
maxWidthDiffRate: { //最大差异率
type: Number,
default: 20
},
bgColor: { //背景色
type: String,
default: ''
},
},
onLoad(option) {
this.isSelfStudy = option.isSelfStudy
// 页面A设置参数
const app = getApp()
if (app.globalData.applyInfo) {
this.applyInfo = app.globalData.applyInfo
} else {
uni.navigateTo({
url: '/pages/meetingList/index'
})
}
this.ctx = uni.createCanvasContext("handWriting");
this.$nextTick(() => {
uni.createSelectorQuery().select('.handCenter').boundingClientRect(rect => {
this.canvasWidth = rect.width;
this.canvasHeight = rect.height;
this.drawBgColor()
})
.exec();
});
},
methods: {
handleBack() {
// 固定跳转首页
uni.navigateBack()
},
readChange(flag) {
this.hasReaded = flag
},
postApply() {
this.readShow = false
// 这里可以添加实际的API调用
this.subCanvas()
},
getPickerColor(color) {
this.showPickerColor = false;
if (color) {
this.lineColor = color;
}
},
// 笔迹开始
uploadScaleStart(e) {
this.canAddHistory = true
this.ctx.setStrokeStyle(this.lineColor)
this.ctx.setLineCap("round") //'butt'、'round'、'square'
},
// 笔迹移动
uploadScaleMove(e) {
let temX = e.changedTouches[0].x
let temY = e.changedTouches[0].y
this.initPoint(temX, temY)
this.onDraw()
},
/**
* 触摸结束
*/
uploadScaleEnd() {
this.canAddHistory = true;
this.points = [];
},
/**
* 记录点属性
*/
initPoint(x, y) {
var point = {
x: x,
y: y,
t: Date.now()
};
var prePoint = this.points.slice(-1)[0];
if (prePoint && (prePoint.t === point.t || prePoint.x === x && prePoint.y === y)) {
return;
}
if (prePoint && this.openSmooth) {
var prePoint2 = this.points.slice(-2, -1)[0];
point.distance = Math.sqrt(Math.pow(point.x - prePoint.x, 2) + Math.pow(point.y - prePoint.y, 2));
point.speed = point.distance / (point.t - prePoint.t || 0.1);
point.lineWidth = this.getLineWidth(point.speed);
if (prePoint2 && prePoint2.lineWidth && prePoint.lineWidth) {
var rate = (point.lineWidth - prePoint.lineWidth) / prePoint.lineWidth;
var maxRate = this.maxWidthDiffRate / 100;
maxRate = maxRate > 1 ? 1 : maxRate < 0.01 ? 0.01 : maxRate;
if (Math.abs(rate) > maxRate) {
var per = rate > 0 ? maxRate : -maxRate;
point.lineWidth = prePoint.lineWidth * (1 + per);
}
}
}
this.points.push(point);
this.points = this.points.slice(-3);
},
/**
* @param {Object}
* 线宽
*/
getLineWidth(speed) {
var minSpeed = this.minSpeed > 10 ? 10 : this.minSpeed < 1 ? 1 : this.minSpeed; //1.5
var addWidth = (this.maxWidth - this.minWidth) * speed / minSpeed;
var lineWidth = Math.max(this.maxWidth - addWidth, this.minWidth);
return Math.min(lineWidth, this.maxWidth);
},
/**
* 绘画逻辑
*/
onDraw() {
if (this.points.length < 2) return;
this.addHistory();
var point = this.points.slice(-1)[0];
var prePoint = this.points.slice(-2, -1)[0];
let that = this
var onDraw = function onDraw() {
if (that.openSmooth) {
that.drawSmoothLine(prePoint, point);
} else {
that.drawNoSmoothLine(prePoint, point);
}
};
if (typeof this.requestAnimationFrame === 'function') {
this.requestAnimationFrame(function() {
return onDraw();
});
} else {
onDraw();
}
},
//添加历史图片地址
addHistory() {
if (!this.maxHistoryLength || !this.canAddHistory) return;
this.canAddHistory = false;
if (!this.getImagePath) {
this.historyList.length++;
return;
}
//历史地址 (暂时无用)
let that = this
that.getImagePath().then(function(url) {
if (url) {
that.historyList.push(url)
that.historyList = that.historyList.slice(-that.maxHistoryLength);
}
});
},
//画平滑线
drawSmoothLine(prePoint, point) {
var dis_x = point.x - prePoint.x;
var dis_y = point.y - prePoint.y;
if (Math.abs(dis_x) + Math.abs(dis_y) <= 2) {
point.lastX1 = point.lastX2 = prePoint.x + dis_x * 0.5;
point.lastY1 = point.lastY2 = prePoint.y + dis_y * 0.5;
} else {
point.lastX1 = prePoint.x + dis_x * 0.3;
point.lastY1 = prePoint.y + dis_y * 0.3;
point.lastX2 = prePoint.x + dis_x * 0.7;
point.lastY2 = prePoint.y + dis_y * 0.7;
}
point.perLineWidth = (prePoint.lineWidth + point.lineWidth) / 2;
if (typeof prePoint.lastX1 === 'number') {
this.drawCurveLine(prePoint.lastX2, prePoint.lastY2, prePoint.x, prePoint.y, point.lastX1, point
.lastY1, point.perLineWidth);
if (prePoint.isFirstPoint) return;
if (prePoint.lastX1 === prePoint.lastX2 && prePoint.lastY1 === prePoint.lastY2) return;
var data = this.getRadianData(prePoint.lastX1, prePoint.lastY1, prePoint.lastX2, prePoint.lastY2);
var points1 = this.getRadianPoints(data, prePoint.lastX1, prePoint.lastY1, prePoint.perLineWidth / 2);
var points2 = this.getRadianPoints(data, prePoint.lastX2, prePoint.lastY2, point.perLineWidth / 2);
this.drawTrapezoid(points1[0], points2[0], points2[1], points1[1]);
} else {
point.isFirstPoint = true;
}
},
//画不平滑线
drawNoSmoothLine(prePoint, point) {
point.lastX = prePoint.x + (point.x - prePoint.x) * 0.5;
point.lastY = prePoint.y + (point.y - prePoint.y) * 0.5;
if (typeof prePoint.lastX === 'number') {
this.drawCurveLine(prePoint.lastX, prePoint.lastY, prePoint.x, prePoint.y, point.lastX, point.lastY,
this.maxWidth);
}
},
//画线
drawCurveLine(x1, y1, x2, y2, x3, y3, lineWidth) {
lineWidth = Number(lineWidth.toFixed(1));
this.ctx.setLineWidth && this.ctx.setLineWidth(lineWidth);
this.ctx.lineWidth = lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(Number(x1.toFixed(1)), Number(y1.toFixed(1)));
this.ctx.quadraticCurveTo(Number(x2.toFixed(1)), Number(y2.toFixed(1)), Number(x3.toFixed(1)), Number(y3
.toFixed(1)));
this.ctx.stroke();
this.ctx.draw && this.ctx.draw(true);
},
//画梯形
drawTrapezoid(point1, point2, point3, point4) {
this.ctx.beginPath();
this.ctx.moveTo(Number(point1.x.toFixed(1)), Number(point1.y.toFixed(1)));
this.ctx.lineTo(Number(point2.x.toFixed(1)), Number(point2.y.toFixed(1)));
this.ctx.lineTo(Number(point3.x.toFixed(1)), Number(point3.y.toFixed(1)));
this.ctx.lineTo(Number(point4.x.toFixed(1)), Number(point4.y.toFixed(1)));
this.ctx.setFillStyle && this.ctx.setFillStyle(this.lineColor);
this.ctx.fillStyle = this.lineColor;
this.ctx.fill();
this.ctx.draw && this.ctx.draw(true);
},
//获取弧度
getRadianData(x1, y1, x2, y2) {
var dis_x = x2 - x1;
var dis_y = y2 - y1;
if (dis_x === 0) {
return {
val: 0,
pos: -1
};
}
if (dis_y === 0) {
return {
val: 0,
pos: 1
};
}
var val = Math.abs(Math.atan(dis_y / dis_x));
if (x2 > x1 && y2 < y1 || x2 < x1 && y2 > y1) {
return {
val: val,
pos: 1
};
}
return {
val: val,
pos: -1
};
},
//获取弧度点
getRadianPoints(radianData, x, y, halfLineWidth) {
if (radianData.val === 0) {
if (radianData.pos === 1) {
return [{
x: x,
y: y + halfLineWidth
}, {
x: x,
y: y - halfLineWidth
}];
}
return [{
y: y,
x: x + halfLineWidth
}, {
y: y,
x: x - halfLineWidth
}];
}
var dis_x = Math.sin(radianData.val) * halfLineWidth;
var dis_y = Math.cos(radianData.val) * halfLineWidth;
if (radianData.pos === 1) {
return [{
x: x + dis_x,
y: y + dis_y
}, {
x: x - dis_x,
y: y - dis_y
}];
}
return [{
x: x + dis_x,
y: y - dis_y
}, {
x: x - dis_x,
y: y + dis_y
}];
},
/**
* 背景色
*/
drawBgColor() {
if (!this.bgColor) return;
this.ctx.setFillStyle && this.ctx.setFillStyle(this.bgColor);
this.ctx.fillStyle = this.bgColor;
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.draw && this.ctx.draw(true);
},
//图片绘制
drawByImage(url) {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
try {
this.ctx.drawImage(url, 0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.draw && this.ctx.draw(true);
} catch (e) {
this.historyList.length = 0;
}
},
/**
* 清空
*/
clear() {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.draw && this.ctx.draw();
this.drawBgColor();
this.historyList.length = 0;
},
//撤消
undo() {
if (!this.getImagePath || !this.historyList.length) return;
var pngURL = this.historyList.splice(-1)[0];
this.drawByImage(pngURL);
if (this.historyList.length === 0) {
this.clear();
}
},
//是否为空
isEmpty() {
return this.historyList.length === 0;
},
/**
* @param {Object} str
* @param {Object} color
* 选择颜色
*/
selectColorEvent(str, color) {
this.selectColor = str;
this.lineColor = color;
this.ctx.setStrokeStyle(this.lineColor)
},
//完成
// 完成
subCanvas() {
if (this.isEmpty()) {
uni.showToast({
title: '没有任何绘制内容哦',
icon: 'none',
});
return;
}
uni.showLoading({
title: '正在生成图片...'
});
uni.canvasToTempFilePath({
canvasId: 'handWriting',
fileType: 'png',
quality: 1,
success: async (res) => {
uni.hideLoading();
// 1. 保存到本地相册
try {
// await this.saveToAlbum(res.tempFilePath);
// 2. 上传到服务器
uni.showLoading({
title: '正在上传签名...'
});
const uploadRes = await this.uploadSignature(res.tempFilePath);
uni.hideLoading();
this.applyInfo.applySign = uploadRes
const response = await post('/api/v1/app_auth/metting-room/order/register', this
.applyInfo)
console.log("===response", response)
if (!response || !response.success) {
throw new Error('会议室预定失败');
}
if (response.success == true) {
uni.redirectTo({
url: `/pages/docList/index?files=${JSON.stringify(response.data)}&&isSelfStudy=${this.isSelfStudy}`
})
}
uni.showToast({
title: '会议室预定成功!',
icon: 'success'
});
// const result = await downloadPdfFiles(response);
// console.log('下载结果:', result);
// if(result.success==true){
// uni.navigateTo({
// url: `/pages/meetingList/index`
// });
// }
// 3. 可以通过回调或者事件将上传结果返回给父组件
// this.$emit('upload-success', {
// tempFilePath: res.tempFilePath,
// serverPath: uploadRes // 假设服务器返回了图片地址
// });
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || '上传失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '生成图片失败',
icon: 'none'
});
console.error(err);
}
});
},
// 保存到相册的方法
saveToAlbum(tempFilePath) {
return new Promise((resolve, reject) => {
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: () => {
uni.showToast({
title: '已保存到相册',
duration: 2000
});
resolve();
},
fail: (err) => {
reject(new Error('保存到相册失败'));
console.error(err);
}
});
});
},
// 上传签名到服务器的方法
uploadSignature(tempFilePath) {
return new Promise((resolve, reject) => {
// 这里替换为你自己的上传接口
const uploadUrl = IMAGE_BASE_URL + '/api/v1/upload';
uni.uploadFile({
url: uploadUrl,
filePath: tempFilePath,
name: 'file',
// formData: {
// // 可以添加其他表单数据
// userId: '123', // 示例用户ID
// type: 'signature'
// },
success: (uploadRes) => {
console.log(uploadRes);
console.log(JSON.parse(uploadRes.data))
try {
const {
success,
data
} = JSON.parse(uploadRes.data);
if (success) {
resolve(data); // 假设服务器返回了图片URL
} else {
reject(new Error(success || '上传失败'));
}
} catch (e) {
reject(new Error('解析服务器响应失败'));
}
},
fail: (err) => {
reject(new Error('上传请求失败'));
console.error(err);
}
});
});
},
//保存到相册
saveCanvasAsImg() {
uni.canvasToTempFilePath({
canvasId: 'handWriting',
fileType: 'png',
quality: 1, //图片质量
success(res) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success(res) {
uni.showToast({
title: '已保存到相册',
duration: 2000
});
}
});
}
});
},
//预览
previewCanvasImg() {
uni.canvasToTempFilePath({
canvasId: 'handWriting',
fileType: 'jpg',
quality: 1, //图片质量
success(res) {
uni.previewImage({
urls: [res.tempFilePath] //预览图片 数组
});
}
});
},
}
};
</script>
<style scoped>
page {
background: #fbfbfb;
height: auto;
overflow: hidden;
}
.wrapper {
width: 100%;
height: 100vh;
/* margin: 40rpx 0; */
overflow: hidden;
display: flex;
align-content: center;
flex-direction: column;
/* justify-content: center; */
font-size: 28rpx;
}
.nav-bar {
display: flex;
align-items: center;
padding: 15px;
// background-color: #fff;
border-bottom: 1px solid #f5f5f5;
padding-top: 60rpx;
}
.nav-left {
display: flex;
align-items: center;
/* margin-right: 10px; */
}
.nav-title {
flex: 1;
text-align: center;
// font-weight: bold;
width: 100%;
}
.handWriting {
background: #fff;
width: 95vw;
height: 240px;
margin: 0 auto;
}
.handRight {
display: inline-flex;
align-items: center;
}
.handCenter {
border: 4rpx dashed #e9e9e9;
height: 240px;
overflow: hidden;
width: 95vw;
box-sizing: border-box;
margin: 10px auto 0px auto;
}
.handTitle {
transform: rotate(90deg);
flex: 1;
color: #666;
}
.handBtn button {
font-size: 28rpx;
}
.handBtn {
height: 10vh;
display: flex;
/* flex-direction: column; */
justify-content: space-between;
align-content: center;
align-items: center;
/* flex: 1; */
}
.delBtn {
/* position: absolute;
top: 250rpx;
left: 0rpx;
transform: rotate(90deg); */
height: 35px;
color: #666;
}
.delBtn image {
position: absolute;
top: 13rpx;
left: 25rpx;
}
.subBtn {
/* position: absolute;
bottom: 52rpx;
left: -3rpx;
display: inline-flex;
transform: rotate(90deg); */
background: #008ef6;
color: #fff;
/* margin-bottom: 30rpx; */
text-align: center;
height: 35px;
/* justify-content: center; */
}
/*Peach - 新增 - 保存*/
.saveBtn {
position: absolute;
top: 375rpx;
left: 0rpx;
transform: rotate(90deg);
color: #666;
}
.previewBtn {
position: absolute;
top: 500rpx;
left: 0rpx;
transform: rotate(90deg);
color: #666;
}
.undoBtn {
/* position: absolute;
top: 625rpx;
left: 0rpx;
transform: rotate(90deg); */
height: 35px;
color: #666;
}
.black-select {
width: 60rpx;
height: 60rpx;
position: absolute;
top: 30rpx;
left: 25rpx;
}
.red-select {
width: 60rpx;
height: 60rpx;
position: absolute;
top: 140rpx;
left: 25rpx;
}
.color_pic {
width: 70rpx;
height: 70rpx;
border-radius: 25px;
position: absolute;
top: 60rpx;
left: 18rpx;
border: 1px solid #ddd;
}
</style>