You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

498 lines
11 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="u-signature">
<view class="u-signature__canvas-wrap">
<canvas
class="u-signature__canvas"
:canvas-id="canvasId"
:disable-scroll="true"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
:style="{
width: canvasWidth + 'px',
height: canvasHeight + 'px',
background: bgColor
}"
></canvas>
</view>
<view v-if="showToolbar" class="u-signature__toolbar">
<view class="u-signature__toolbar-icons u-flex u-flex-x">
<view class="u-signature__toolbar-icon" @click="undo">
<up-icon name="arrow-left" size="22" :color="pathStack.length === 0 ? '#ccc' : '#999'"></up-icon>
</view>
<view class="u-signature__toolbar-icon" @click="clear">
<up-icon name="trash" size="25" color="#999"></up-icon>
</view>
<view class="u-signature__toolbar-icon" @click="toggleBrushSettings">
<up-icon name="edit-pen" size="25" color="#999"></up-icon>
</view>
<view class="u-signature__toolbar-icon" @click="toggleColorSettings">
<up-icon name="grid" size="24" color="#999"></up-icon>
</view>
<view class="u-signature__toolbar-icon" @click="exportSignature">
<up-icon name="checkmark" size="25" :color="isEmpty ? '#ccc' : '#999'"></up-icon>
</view>
</view>
<!-- 笔画设置 -->
<view v-if="showBrushSettings" class="u-signature__brush-settings">
<view class="u-signature__progress">
<text class="u-signature__progress-label">笔画大小:</text>
<up-slider
v-model="lineWidth"
:min="1"
:max="20"
:step="1"
@show-value="true"
:value-show="(lineWidth)"
></up-slider>
</view>
</view>
<!-- 颜色设置 -->
<view v-if="showColorSettings" class="u-signature__color-settings">
<view class="u-signature__color-picker">
<text class="u-signature__color-label">笔画颜色:</text>
<view class="u-signature__colors">
<view
v-for="(color, index) in presetColors"
:key="index"
class="u-signature__color-item"
:class="{'u-signature__color-item--active': lineColor === color}"
:style="{ backgroundColor: color }"
@click="selectColor(color)"
></view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'u-signature',
props: {
// 画布宽度
width: {
type: [String, Number],
default: 300
},
// 画布高度
height: {
type: [String, Number],
default: 200
},
// 背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
// 默认笔画颜色
color: {
type: String,
default: '#000000'
},
// 默认笔画粗细
thickness: {
type: [String, Number],
default: 3
},
// 是否显示工具栏
showToolbar: {
type: Boolean,
default: true
}
},
data() {
return {
canvasId: 'u-signature-' + Math.random().toString(36).substr(2, 9),
canvasWidth: 300,
canvasHeight: 200,
lineColor: '#000000',
lineWidth: 3,
isDrawing: false,
pathStack: [], // 存储绘制路径用于回退
currentPath: [], // 当前绘制路径
ctx: null,
isEmpty: true,
presetColors: [
'#000000', // 黑色
'#ff0000', // 红色
'#00ff00', // 绿色
'#0000ff', // 蓝色
'#ffff00', // 黄色
'#00ffff', // 青色
'#ff00ff', // 紫色
'#ffffff' // 白色
],
showBrushSettings: false,
showColorSettings: false,
lastPoint: null // 保存上一个点的坐标
}
},
mounted() {
this.initCanvas()
},
watch: {
width: {
handler(newVal) {
this.canvasWidth = Number(newVal)
},
immediate: true
},
height: {
handler(newVal) {
this.canvasHeight = Number(newVal)
},
immediate: true
},
color: {
handler(newVal) {
this.lineColor = newVal
},
immediate: true
},
thickness: {
handler(newVal) {
this.lineWidth = Number(newVal)
},
immediate: true
}
},
methods: {
initCanvas() {
// #ifndef APP-NVUE
const ctx = uni.createCanvasContext(this.canvasId, this)
this.ctx = ctx
this.clearCanvas()
// #endif
// #ifdef APP-NVUE
// NVUE环境下的处理
// #endif
},
touchStart(e) {
if (!this.ctx) return
this.isDrawing = true
this.isEmpty = false
this.currentPath = []
const { x, y } = this.getCanvasPoint(e)
this.ctx.beginPath()
this.ctx.moveTo(x, y)
this.ctx.setLineCap('round')
this.ctx.setLineJoin('round')
this.ctx.setStrokeStyle(this.lineColor)
this.ctx.setLineWidth(this.lineWidth)
// 记录起始点
this.currentPath.push({
x,
y,
type: 'start',
color: this.lineColor,
width: this.lineWidth
})
// 保存上一个点
this.lastPoint = { x, y }
// 阻止默认事件以提高性能
e.preventDefault()
},
touchMove(e) {
if (!this.isDrawing || !this.ctx) return
// 阻止默认事件以提高性能
e.preventDefault()
const { x, y } = this.getCanvasPoint(e)
// 使用更密集的点采样确保线条连贯性
if (this.lastPoint) {
// 计算两点间距离
const distance = Math.sqrt(Math.pow(x - this.lastPoint.x, 2) + Math.pow(y - this.lastPoint.y, 2))
// 根据距离确定插值点数量确保点间距不超过1像素以获得更平滑的线条
const steps = Math.max(1, Math.floor(distance / 1))
// 在两点间插入插值点
for (let i = 1; i <= steps; i++) {
const t = i / steps
const midX = this.lastPoint.x + (x - this.lastPoint.x) * t
const midY = this.lastPoint.y + (y - this.lastPoint.y) * t
this.ctx.lineTo(midX, midY)
this.ctx.stroke()
this.currentPath.push({
x: midX,
y: midY,
type: 'move'
})
}
} else {
this.ctx.lineTo(x, y)
this.ctx.stroke()
this.currentPath.push({
x,
y,
type: 'move'
})
}
this.ctx.draw(true)
// 更新上一个点
this.lastPoint = { x, y }
},
touchEnd(e) {
if (!this.isDrawing || !this.ctx) return
this.isDrawing = false
this.ctx.closePath()
this.lastPoint = null
// 将当前路径加入栈中用于回退
if (this.currentPath.length > 0) {
this.pathStack.push([...this.currentPath])
}
},
// 获取相对于canvas的坐标点
getCanvasPoint(e) {
const touch = e.touches[0]
const rect = uni.createSelectorQuery().in(this).select('.u-signature__canvas').boundingClientRect()
return new Promise((resolve) => {
rect.boundingClientRect(data => {
const x = touch.x - data.left
const y = touch.y - data.top
resolve({ x, y })
}).exec()
})
},
// 同步获取canvas坐标点兼容处理
getCanvasPoint(e) {
const touch = e.touches[0]
const canvas = uni.createSelectorQuery().in(this).select('.u-signature__canvas')
// 返回一个包含坐标的对象
return {
x: touch.x,
y: touch.y
}
},
// 选择颜色
selectColor(color) {
this.lineColor = color
},
// 回退操作
undo() {
if (this.pathStack.length === 0) return
// 弹出最后一个路径
this.pathStack.pop()
// 重新绘制
this.redraw()
},
// 重新绘制所有路径
redraw() {
this.clearCanvas()
if (this.pathStack.length === 0) {
this.isEmpty = true
return
}
this.isEmpty = false
// #ifndef APP-NVUE
this.pathStack.forEach(path => {
if (path.length === 0) return
this.ctx.beginPath()
this.ctx.setLineCap('round')
this.ctx.setLineJoin('round')
let lastPoint = null
path.forEach((point, index) => {
if (index === 0 && point.type === 'start') {
// 设置起始点样式
this.ctx.setStrokeStyle(point.color)
this.ctx.setLineWidth(point.width)
this.ctx.moveTo(point.x, point.y)
lastPoint = { x: point.x, y: point.y }
} else if (point.type === 'move') {
this.ctx.lineTo(point.x, point.y)
lastPoint = { x: point.x, y: point.y }
}
})
this.ctx.stroke()
this.ctx.draw(true)
})
// #endif
},
// 清空画布
clear() {
this.pathStack = []
this.currentPath = []
this.isEmpty = true
this.lastPoint = null
this.clearCanvas()
},
// 清空画布内容
clearCanvas() {
if (!this.ctx) return
// #ifndef APP-NVUE
this.ctx.setFillStyle(this.bgColor)
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
this.ctx.draw()
// #endif
},
// 导出签名图片
exportSignature() {
if (this.isEmpty) return
// #ifndef APP-NVUE
uni.canvasToTempFilePath({
canvasId: this.canvasId,
fileType: 'png',
quality: 1,
success: (res) => {
this.$emit('confirm', res.tempFilePath)
},
fail: (err) => {
this.$emit('error', err)
}
}, this)
// #endif
// #ifdef APP-NVUE
// NVUE环境下可能需要特殊处理
// #endif
},
// 切换笔画设置显示
toggleBrushSettings() {
this.showBrushSettings = !this.showBrushSettings;
if (this.showBrushSettings) {
this.showColorSettings = false;
}
},
// 切换颜色设置显示
toggleColorSettings() {
this.showColorSettings = !this.showColorSettings;
if (this.showColorSettings) {
this.showBrushSettings = false;
}
},
}
}
</script>
<style lang="scss" scoped>
.u-signature {
display: flex;
flex-direction: column;
&__canvas-wrap {
border: 1px solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
&__canvas {
width: 100%;
height: 100%;
}
&__toolbar {
margin-top: 5px;
background-color: #fff;
}
&__toolbar-icons {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1px 0;
// border: 1px solid #e0e0e0;
border-radius: 4px;
}
&__toolbar-icon {
padding: 5px;
}
&__brush-settings,
&__color-settings {
margin-top: 15px;
padding: 1px;
// border: 1px solid #e0e0e0;
border-radius: 4px;
}
&__progress {
&-label {
display: block;
margin-bottom: 10px;
font-size: 14px;
color: #999;
}
}
&__color-picker {
margin-bottom: 10px;
}
&__color-label {
display: block;
margin-bottom: 10px;
font-size: 14px;
color: #999;
}
&__colors {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
}
&__color-item {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid #f0f0f0;
cursor: pointer;
&--active {
border-color: #2979ff;
transform: scale(1.1);
}
}
&__actions {
display: flex;
flex-direction: row;
gap: 10px;
justify-content: center;
}
}
</style>