pet/uni_modules/uview-next/components/u-signature/u-signature.vue

772 lines
25 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="u-signature"
:class="[landscape ? 'u-signature-landscape' : '']"
:style="[containerStyle, $u.addStyle(customStyle)]"
@touchmove.prevent.stop
@wheel.prevent.stop
>
<view v-if="showTitle" class="u-signature__title" :class="[{'u-signature__title-fixed': fixed}]">
<slot name="title">
<text class="u-signature__title-text">{{ title }}</text>
</slot>
</view>
<view class="u-signature__canvas">
<canvas
:type="type"
:canvas-id="canvasId"
:id="canvasId"
:style="[canvasStyle]"
:class="['u-signature__canvas']"
:disable-scroll="disableScroll"
@touchstart="onCanvasTouchStart"
@touchmove="onCanvasTouchMove"
@touchend="onCanvasTouchEnd"
@touchcancel="onCanvasTouchCancel" >
</canvas>
</view>
<view v-if="showToolbar"
class="u-signature__toolbar"
:class="[{'u-signature__toolbar-fixed': fixed}]"
:style="[$u.addStyle(toolbarStyle)]"
>
<slot name="toolbar">
<view class="u-signature__toolbar-left">
<view v-if="showColorList" class="u-signature__toolbar-item">
<view class="u-signature__toolbar-color-list">
<view
v-for="(item, index) in penColorList"
:key="index"
class="u-signature__toolbar-color"
:style="[
{
backgroundColor: penColorInner === item ? 'transparent' : item,
borderColor: item,
},
]"
@click="handlePenColor(item)"
></view>
</view>
</view>
</view>
<view class="u-signature__toolbar-right">
<view class="u-signature__toolbar-item">
<u-button
type="info"
icon="trash"
@click="handleClear"
>
{{ clearText }}
</u-button>
</view>
<view class="u-signature__toolbar-item">
<u-button
type="info"
icon="back"
@click="handleUndo"
>
{{ undoText }}
</u-button>
</view>
<view class="u-signature__toolbar-item">
<u-button
type="primary"
@click="handleConfirm"
>
{{ confirmText }}
</u-button>
</view>
</view>
</slot>
</view>
</view>
</template>
<script>
import props from './props.js';
import mixin from '../../libs/mixin/mixin';
import mpMixin from '../../libs/mixin/mpMixin';
/**
* Signature 签名组件
* @description 可用于业务签名等场景
* @tutorial https://uview.d3u.cn/components/signature.html
*
* @property {Number} penSize 画笔大小 (默认 2
* @property {Number} minLineWidth 线条最小宽度 (默认 2
* @property {Number} maxLineWidth 线条最大宽度 (默认 6
* @property {String} penColor 画笔颜色 (默认 'black'
* @property {String} backgroundColor 背景颜色 (默认 ''
* @property {String} type canvas类型 (默认 '2d'
* @property {Boolean} openSmooth 是否开启压感 (默认 false
* @property {Number} maxHistoryLength 最大历史记录数 (默认 20
* @property {Boolean} landscape 是否横屏 (默认 false
* @property {Boolean} disableScroll 是否禁用滚动 (默认 true
* @property {Boolean} disabled 是否禁用 (默认 false
* @property {Boolean} boundingBox 只生成内容区域 (默认 false
* @property {Object} customStyle 自定义样式
* @event {Function} undo 撤销方法
* @event {Function} clear 清空方法
* @event {Function} getImage 保存方法
* @example <u-signature :penColor="penColor" :penSize="penSize" ref="signatureRef"></u-signature>
*/
let canvasObj = {};
export default {
name: 'u-signature',
mixins: [mpMixin, mixin, props],
data() {
return {
canvasId: 'signatureId_' + uni.$u.guid(),
ctx: null,
canvas: null,
canvasWidth: 0,
canvasHeight: 0,
isDrawing: false,
lastPoint: null,
currentStroke: [],
history: [],
isEmpty: true,
velocityFilterWeight: 0.7,
minVelocity: 0.25,
currentVelocity: 0,
lastTimestamp: 0,
penColorInner: ''
};
},
computed: {
is2d() {
// #ifdef MP-WEIXIN
return this.type == '2d';
// #endif
// #ifndef MP-WEIXIN
return false;
// #endif
},
containerStyle() {
const style = {
width: '100%',
height: '100%',
};
return style;
},
canvasStyle() {
const style = {
width: '100%',
height: '100%',
display: 'block',
};
if (
this.backgroundColor &&
this.backgroundColor !== 'transparent'
) {
style.backgroundColor = this.backgroundColor;
}
return style;
},
},
watch: {
penColor: {
immediate: true,
handler(newVal) {
this.penColorInner = newVal;
},
}
},
mounted() {
this.init();
},
// #ifdef VUE3
emits: ['clear', 'undo', 'confirm'],
// #endif
methods: {
async init() {
await this.$nextTick();
const query = uni
.createSelectorQuery()
.in(this)
.select(`#${this.canvasId}`);
if (this.is2d) {
let canvas = await new Promise((resolve) => {
query
.fields({
node: true,
size: true,
})
.exec((res) => {
this.canvasWidth = parseInt(res[0].width);
this.canvasHeight = parseInt(res[0].height);
resolve(res[0].node);
});
});
canvas.width = this.canvasWidth;
canvas.height = this.canvasHeight;
this.ctx = canvas.getContext('2d');
canvasObj[this.canvasId] = canvas;
} else {
// 传统 canvas 模式
// #ifdef MP-ALIPAY
this.ctx = uni.createCanvasContext(this.canvasId);
// #endif
// #ifndef MP-ALIPAY
this.ctx = uni.createCanvasContext(this.canvasId, this);
// #endif
await new Promise((resolve) => {
query
.boundingClientRect((data) => {
this.canvasWidth = parseInt(data.width);
this.canvasHeight = parseInt(data.height);
resolve();
})
.exec();
});
}
// 设置画笔样式
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.strokeStyle = this.penColorInner;
this.ctx.lineWidth = this.penSize;
// 绘制背景和水印
this.drawBackgroundAndWatermark();
},
// 开始绘制
onCanvasTouchStart(e) {
if (this.disabled) return;
const touch = e.touches[0];
const point = this.getTouchPoint(touch);
this.isDrawing = true;
this.lastPoint = point;
this.currentStroke = [point];
this.lastTimestamp = Date.now();
this.currentVelocity = 0;
},
// 绘制中
onCanvasTouchMove(e) {
if (this.disabled || !this.isDrawing) return;
const touch = e.touches[0];
const point = this.getTouchPoint(touch);
const now = Date.now();
// 计算速度(用于压感)
if (this.openSmooth && this.lastPoint) {
const distance = this.getDistance(this.lastPoint, point);
const timeDelta = now - this.lastTimestamp;
const velocity = distance / timeDelta;
this.currentVelocity =
this.velocityFilterWeight * velocity +
(1 - this.velocityFilterWeight) * this.currentVelocity;
}
this.drawLine(this.lastPoint, point);
this.lastPoint = point;
this.currentStroke.push(point);
this.lastTimestamp = now;
this.isEmpty = false;
},
// 结束绘制
onCanvasTouchEnd(e) {
if (this.disabled || !this.isDrawing) return;
this.isDrawing = false;
// 保存到历史记录
if (this.currentStroke.length > 0) {
this.saveToHistory();
}
},
// 取消绘制
onCanvasTouchCancel(e) {
this.onCanvasTouchEnd(e);
},
// 获取触摸点坐标
getTouchPoint(touch) {
return {
x: touch.x,
y: touch.y,
timestamp: Date.now(),
};
},
// 绘制线条
drawLine(from, to) {
if (!this.ctx || !from || !to) return;
let lineWidth = this.penSize;
// 压感效果
if (this.openSmooth) {
const velocity = Math.max(
this.currentVelocity,
this.minVelocity,
);
lineWidth = Math.max(
this.minLineWidth,
Math.min(this.maxLineWidth, this.penSize / velocity),
);
}
this.ctx.beginPath();
this.ctx.moveTo(from.x, from.y);
this.ctx.lineTo(to.x, to.y);
this.ctx.strokeStyle = this.penColorInner;
this.ctx.lineWidth = lineWidth;
this.ctx.stroke();
if (!this.is2d) {
this.ctx.draw(true);
}
},
// 计算两点距离
getDistance(p1, p2) {
return Math.sqrt(
Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2),
);
},
// 绘制水印
drawWatermark() {
if (!this.ctx || !this.watermark.text) return;
const ctx = this.ctx;
const text = this.watermark.text;
const fontSize = parseInt(this.watermark.fontSize);
const fontFamily = this.watermark.fontFamily;
const color = this.watermark.color;
const rotate = this.watermark.rotate;
const spacing = this.watermark.spacing;
// 设置水印样式
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 计算文本尺寸
const textMetrics = ctx.measureText(text);
const textWidth = textMetrics.width;
const textHeight = fontSize;
if (this.watermark.single) {
// 绘制单个居中水印
const centerX = this.canvasWidth / 2;
const centerY = this.canvasHeight / 2;
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate((rotate * Math.PI) / 180);
ctx.fillText(text, 0, 0);
ctx.restore();
} else {
// 绘制网格水印
const cols = Math.ceil(this.canvasWidth / spacing) + 1;
const rows = Math.ceil(this.canvasHeight / spacing) + 1;
// 绘制水印网格
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * spacing;
const y = row * spacing;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotate * Math.PI) / 180);
ctx.fillText(text, 0, 0);
ctx.restore();
}
}
}
// 对于非2d canvas需要调用draw方法
if (!this.is2d) {
ctx.draw(true);
}
},
// 重绘历史记录
redrawHistory() {
if (!this.ctx || !this.history.length) return;
this.history.forEach((item) => {
if (item.stroke && item.stroke.length > 1) {
this.ctx.beginPath();
this.ctx.moveTo(item.stroke[0].x, item.stroke[0].y);
for (let i = 1; i < item.stroke.length; i++) {
this.ctx.lineTo(item.stroke[i].x, item.stroke[i].y);
}
this.ctx.strokeStyle = this.penColorInner;
this.ctx.lineWidth = this.penSize;
this.ctx.stroke();
}
});
},
// 清空画布并设置背景
clearCanvas() {
if (!this.ctx) return;
// 清空画布
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 重新设置背景色
if (
this.backgroundColor &&
this.backgroundColor !== 'transparent'
) {
this.ctx.fillStyle = this.backgroundColor;
this.ctx.fillRect(
0,
0,
this.canvasWidth,
this.canvasHeight,
);
}
},
// 绘制背景和水印
drawBackgroundAndWatermark() {
this.clearCanvas();
// 重新绘制水印
if (this.showWatermark && this.watermark.text) {
this.drawWatermark();
}
},
// 恢复水印
restoreWatermark() {
if (!this.ctx) return;
// 绘制背景和水印
this.drawBackgroundAndWatermark();
// 重绘所有笔画
this.redrawHistory();
// 对于非2d canvas需要调用draw方法
if (!this.is2d) {
this.ctx.draw(true);
}
},
// 保存到历史记录
saveToHistory() {
if (this.maxHistoryLength <= 0) return;
const imageData = {
stroke: [...this.currentStroke],
timestamp: Date.now(),
};
this.history.push(imageData);
// 限制历史记录数量
if (this.history.length > this.maxHistoryLength) {
this.history.shift();
}
this.currentStroke = [];
},
handlePenColor(color){
this.penColorInner = color;
this.ctx.strokeStyle = color;
},
handleClear(){
this.clear();
this.$emit('clear');
},
handleUndo(){
this.undo();
this.$emit('undo');
},
handleConfirm(){
this.getImage().then(res => {
this.$emit('confirm', res);
});
},
// 撤销
undo() {
if (this.history.length === 0) return;
this.history.pop();
this.redrawFromHistory();
},
// 重做(从历史记录重绘)
redrawFromHistory() {
if (!this.ctx) return;
// 绘制背景和水印
this.drawBackgroundAndWatermark();
// 重绘所有笔画
this.redrawHistory();
// 对于非2d canvas需要调用draw方法
if (!this.is2d) {
this.ctx.draw(true);
}
this.isEmpty = this.history.length === 0;
},
// 清空
clear() {
if (!this.ctx) return;
// 绘制背景和水印
this.drawBackgroundAndWatermark();
//对于非2d canvas需要调用draw方法
if (!this.is2d) {
this.ctx.draw(true);
}
this.history = [];
this.currentStroke = [];
this.isEmpty = true;
// 触发清空事件
this.$emit('clear');
},
// 导出图片
getImage() {
return new Promise((resolve, reject) => {
if (this.isEmpty) {
uni.showToast({
title: '签名板为空',
icon: 'none',
});
reject('签名板为空');
return;
}
// 如果保存时不显示水印,需要临时隐藏水印
let needRestoreWatermark = false;
if (this.showWatermark && !this.watermark.showOnSave) {
needRestoreWatermark = true;
// 临时隐藏水印,只绘制背景和笔画
this.clearCanvas();
this.redrawHistory();
// 对于非2d canvas需要调用draw方法
if (!this.is2d) {
this.ctx.draw(true);
}
}
let params = {
canvas: canvasObj[this.canvasId],
canvasId: this.canvasId,
width: this.canvasWidth,
height: this.canvasHeight,
fileType: this.fileType,
quality: this.quality,
success: (res) => {
if (needRestoreWatermark) {
this.restoreWatermark();
}
resolve(res.tempFilePath);
},
fail: (err) => {
if (needRestoreWatermark) {
this.restoreWatermark();
}
reject(err);
},
};
// 处理boundingBox
if (this.boundingBox) {
params.x = 0;
params.y = 0;
}
// #ifdef MP-ALIPAY
uni.canvasToTempFilePath(params);
// #endif
// #ifndef MP-ALIPAY
uni.canvasToTempFilePath(params, this);
// #endif
});
},
},
};
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-signature {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
// 横屏模式
&-landscape {
flex-direction: row-reverse;
}
&__title{
text-align: center;
font-size: 15px;
color: #333;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
&-fixed{
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 9999;
background-color: rgba(255, 255, 255, 0.8);
}
// 横屏模式标题样式
.u-signature-landscape & {
width: 26px;
height: 100%;
flex-direction: column;
align-items: center;
flex-shrink: 0;
&-text {
width: 100vh;
transform: rotate(90deg);
transform-origin: center center;
}
}
}
&__canvas {
width: 100%;
display: block;
touch-action: none;
flex: 1;
}
&__toolbar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 10px 20px 10px;
&-fixed{
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
background-color: rgba(255, 255, 255, 0.8);
}
// 横屏模式工具栏样式
.u-signature-landscape & {
padding: 10px;
width: 34px;
height: 100%;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
&-color-list{
display: flex;
flex-direction: row;
align-items: center;
// 横屏模式颜色列表样式
.u-signature-landscape & {
flex-direction: column;
margin-bottom: 10px;
}
}
&-color{
width: 15px;
height: 15px;
border-radius: 100px;
margin: 0 3px;
border: 6px solid #fff;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1);
// 横屏模式下的颜色选择器样式
.u-signature-landscape & {
margin: 5px 0;
}
}
&-left {
display: flex;
flex-direction: row;
align-items: center;
// 横屏模式左侧工具栏样式
.u-signature-landscape & {
flex-direction: column;
margin-top: 50px;
}
}
&-right {
display: flex;
flex-direction: row;
//align-items: center;
// 横屏模式右侧工具栏样式
.u-signature-landscape & {
flex-direction: column;
margin-bottom: 30px;
.u-signature__toolbar-item {
margin-right: 0;
margin-left: -40px;
height: 100px;
transform: rotate(90deg);
transform-origin: center center;
}
}
}
&-item {
margin-right: 10px;
}
&-item:last-child {
margin-right: 0;
}
}
}
</style>