更新:登录功能
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
@import '../common/abstracts/variable';
|
||||
@import '../common/abstracts/mixin';
|
||||
|
||||
@include b(signature) {
|
||||
@include e(content) {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
background: $-signature-bg;
|
||||
border-radius: $-signature-radius;
|
||||
border: $-signature-border;
|
||||
}
|
||||
|
||||
@include e(content-canvas) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@include e(footer) {
|
||||
margin-top: $-signature-footer-margin-top;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
|
||||
:deep(){
|
||||
.wd-button{
|
||||
margin-left: $-signature-button-margin-left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
263
uni_modules/wot-design-uni/components/wd-signature/types.ts
Normal file
263
uni_modules/wot-design-uni/components/wd-signature/types.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* @Author: 810505339
|
||||
* @Date: 2025-01-10 20:03:57
|
||||
* @LastEditors: weisheng
|
||||
* @LastEditTime: 2025-03-23 16:35:14
|
||||
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-signature/types.ts
|
||||
* 记得注释
|
||||
*/
|
||||
import type { ComponentPublicInstance, ExtractPropTypes } from 'vue'
|
||||
import { baseProps, numericProp } from '../common/props'
|
||||
|
||||
export const signatureProps = {
|
||||
...baseProps,
|
||||
/**
|
||||
* 签名笔颜色
|
||||
* 类型:string
|
||||
* 默认值:#000
|
||||
*/
|
||||
penColor: {
|
||||
type: String,
|
||||
default: '#000'
|
||||
},
|
||||
/**
|
||||
* 签名笔宽度
|
||||
* 类型:number
|
||||
* 默认值:3
|
||||
*/
|
||||
lineWidth: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
/**
|
||||
* 清空按钮的文本
|
||||
* 类型:string
|
||||
*/
|
||||
clearText: String,
|
||||
/**
|
||||
* 撤回按钮的文本
|
||||
* 类型:string
|
||||
*/
|
||||
revokeText: String,
|
||||
/**
|
||||
* 恢复按钮的文本
|
||||
* 类型:string
|
||||
*/
|
||||
restoreText: String,
|
||||
/**
|
||||
* 确认按钮的文本
|
||||
* 类型:string
|
||||
*/
|
||||
confirmText: String,
|
||||
/**
|
||||
* 目标文件的类型
|
||||
* 类型:string
|
||||
* 默认值:png
|
||||
*/
|
||||
fileType: {
|
||||
type: String,
|
||||
default: 'png'
|
||||
},
|
||||
/**
|
||||
* 目标文件的质量
|
||||
* 类型:number
|
||||
* 默认值:1
|
||||
*/
|
||||
quality: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/**
|
||||
* 导出图片的缩放比例
|
||||
* 类型:number
|
||||
* 默认值:1
|
||||
*/
|
||||
exportScale: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/**
|
||||
* 是否禁用签名板
|
||||
* 类型:boolean
|
||||
* 默认值:false
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 画布的高度
|
||||
* 类型:number
|
||||
*/
|
||||
height: numericProp,
|
||||
/**
|
||||
* 画布的宽度
|
||||
* 类型:number
|
||||
*/
|
||||
width: numericProp,
|
||||
/**
|
||||
* 画板的背景色
|
||||
* 类型:string
|
||||
*/
|
||||
backgroundColor: String,
|
||||
/**
|
||||
* 是否禁用画布滚动
|
||||
* 类型:boolean
|
||||
* 默认值:true
|
||||
*/
|
||||
disableScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否开启历史记录
|
||||
* 类型:boolean
|
||||
* 默认值:false
|
||||
*/
|
||||
enableHistory: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 撤回和恢复的步长
|
||||
* 类型:number
|
||||
* 默认值:1
|
||||
*/
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/**
|
||||
* 撤回按钮的文本
|
||||
* 类型:string
|
||||
* 默认值:撤销
|
||||
*/
|
||||
undoText: String,
|
||||
/**
|
||||
* 恢复按钮的文本
|
||||
* 类型:string
|
||||
* 默认值:恢复
|
||||
*/
|
||||
redoText: String,
|
||||
/**
|
||||
* 是否启用压感模式(笔锋)
|
||||
* 类型:boolean
|
||||
* 默认值:false
|
||||
*/
|
||||
pressure: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 压感模式下笔画最小宽度
|
||||
* 类型:number
|
||||
* 默认值:2
|
||||
*/
|
||||
minWidth: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
/**
|
||||
* 压感模式下笔画最大宽度
|
||||
* 类型:number
|
||||
* 默认值:6
|
||||
*/
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: 6
|
||||
},
|
||||
/**
|
||||
* 最小速度阈值,影响压感模式下的笔画宽度变化
|
||||
* 类型:number
|
||||
* 默认值:1.5
|
||||
*/
|
||||
minSpeed: {
|
||||
type: Number,
|
||||
default: 1.5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 签名结果类型
|
||||
* @property tempFilePath - 生成图片的临时路径
|
||||
* @property success - 是否成功生成图片
|
||||
* @property width - 生成图片的宽度
|
||||
* @property height - 生成图片的高度
|
||||
*/
|
||||
export type SignatureResult = {
|
||||
tempFilePath: string
|
||||
success: boolean
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 签名线条类型
|
||||
* @property points - 线条所包含的所有点的数组
|
||||
* @property color - 线条颜色
|
||||
* @property width - 线条宽度
|
||||
* @property backgroundColor - 线条背景色 (可选)
|
||||
* @property isPressure - 是否为笔锋模式的线条 (可选)
|
||||
*/
|
||||
export interface Line {
|
||||
points: Point[]
|
||||
color: string
|
||||
width: number
|
||||
backgroundColor?: string
|
||||
isPressure?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 签名点位类型
|
||||
* @property x - 点的横坐标
|
||||
* @property y - 点的纵坐标
|
||||
* @property t - 点的时间戳
|
||||
* @property speed - 当前点的绘制速度 (可选)
|
||||
* @property distance - 与上一个点的距离 (可选)
|
||||
* @property lineWidth - 当前点的线宽 (可选,用于笔锋模式)
|
||||
* @property lastX1 - 贝塞尔曲线第一个控制点的x坐标 (可选)
|
||||
* @property lastY1 - 贝塞尔曲线第一个控制点的y坐标 (可选)
|
||||
* @property lastX2 - 贝塞尔曲线第二个控制点的x坐标 (可选)
|
||||
* @property lastY2 - 贝塞尔曲线第二个控制点的y坐标 (可选)
|
||||
* @property isFirstPoint - 是否为线条的第一个点 (可选)
|
||||
*/
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
t: number
|
||||
speed?: number
|
||||
distance?: number
|
||||
lineWidth?: number
|
||||
lastX1?: number
|
||||
lastY1?: number
|
||||
lastX2?: number
|
||||
lastY2?: number
|
||||
isFirstPoint?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 签名组件暴露的方法类型
|
||||
* @property init - 初始化签名板
|
||||
* @property clear - 清除签名
|
||||
* @property confirm - 确认签名并生成图片
|
||||
* @property restore - 恢复上一步操作
|
||||
* @property revoke - 撤销上一步操作
|
||||
*/
|
||||
export type SignatureExpose = {
|
||||
/** 初始化签名板
|
||||
* @param forceUpdate - 是否强制更新
|
||||
*/
|
||||
init: (forceUpdate?: boolean) => void
|
||||
/** 点击清除按钮清除签名 */
|
||||
clear: () => void
|
||||
/** 点击确定按钮 */
|
||||
confirm: () => void
|
||||
/* 点击恢复 */
|
||||
restore: () => void
|
||||
/* 点击撤回 */
|
||||
revoke: () => void
|
||||
}
|
||||
|
||||
export type SignatureProps = ExtractPropTypes<typeof signatureProps>
|
||||
|
||||
export type SignatureInstance = ComponentPublicInstance<SignatureExpose, SignatureProps>
|
||||
@@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<view class="wd-signature">
|
||||
<view class="wd-signature__content">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<canvas
|
||||
class="wd-signature__content-canvas"
|
||||
:style="canvasStyle"
|
||||
:width="canvasState.canvasWidth"
|
||||
:height="canvasState.canvasHeight"
|
||||
:canvas-id="canvasId"
|
||||
:id="canvasId"
|
||||
:disable-scroll="disableScroll"
|
||||
@touchstart="startDrawing"
|
||||
@touchend="stopDrawing"
|
||||
@touchmove="draw"
|
||||
type="2d"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<canvas
|
||||
class="wd-signature__content-canvas"
|
||||
:canvas-id="canvasId"
|
||||
:style="canvasStyle"
|
||||
:width="canvasState.canvasWidth"
|
||||
:height="canvasState.canvasHeight"
|
||||
:id="canvasId"
|
||||
:disable-scroll="disableScroll"
|
||||
@touchstart="startDrawing"
|
||||
@touchend="stopDrawing"
|
||||
@touchmove="draw"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<view class="wd-signature__footer">
|
||||
<slot
|
||||
name="footer"
|
||||
:clear="clear"
|
||||
:confirm="confirmSignature"
|
||||
:current-step="currentStep"
|
||||
:revoke="revoke"
|
||||
:restore="restore"
|
||||
:can-undo="lines.length > 0"
|
||||
:can-redo="redoLines.length > 0"
|
||||
:history-list="lines"
|
||||
>
|
||||
<block v-if="enableHistory">
|
||||
<wd-button size="small" plain @click="revoke" :disabled="lines.length <= 0">
|
||||
{{ revokeText || translate('revokeText') }}
|
||||
</wd-button>
|
||||
<wd-button size="small" plain @click="restore" :disabled="redoLines.length <= 0">
|
||||
{{ restoreText || translate('restoreText') }}
|
||||
</wd-button>
|
||||
</block>
|
||||
<wd-button size="small" plain @click="clear">{{ clearText || translate('clearText') }}</wd-button>
|
||||
<wd-button size="small" @click="confirmSignature">{{ confirmText || translate('confirmText') }}</wd-button>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'wd-signature',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { computed, getCurrentInstance, onBeforeMount, onMounted, reactive, ref, watch, type CSSProperties } from 'vue'
|
||||
import { addUnit, getRect, isDef, objToStyle, uuid } from '../common/util'
|
||||
import { signatureProps, type SignatureExpose, type SignatureResult, type Point, type Line } from './types'
|
||||
import { useTranslate } from '../composables/useTranslate'
|
||||
// #ifdef MP-WEIXIN
|
||||
import { canvas2dAdapter } from '../common/canvasHelper'
|
||||
// #endif
|
||||
|
||||
const props = defineProps(signatureProps)
|
||||
const emit = defineEmits(['start', 'end', 'signing', 'confirm', 'clear'])
|
||||
const { translate } = useTranslate('signature')
|
||||
const { proxy } = getCurrentInstance() as any
|
||||
const canvasId = ref<string>(`signature${uuid()}`) // canvas 组件的唯一标识符
|
||||
let canvas: null = null //canvas对象 微信小程序生成图片必须传入
|
||||
const drawing = ref<boolean>(false) // 是否正在绘制
|
||||
const pixelRatio = ref<number>(1) // 像素比
|
||||
|
||||
const canvasState = reactive({
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
ctx: null as UniApp.CanvasContext | null // canvas上下文
|
||||
})
|
||||
|
||||
/**
|
||||
* 判断颜色是否为透明色
|
||||
* @param color 颜色值(支持 rgba/hsla/hex/transparent)
|
||||
*/
|
||||
function isTransparentColor(color: string | undefined): boolean {
|
||||
if (!color) return true
|
||||
const transparentKeywords = ['transparent', '#0000', '#00000000']
|
||||
|
||||
// 标准透明关键字
|
||||
if (transparentKeywords.includes(color.toLowerCase())) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 匹配 rgba(r, g, b, a)
|
||||
const rgbaMatch = color.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*(\d*\.?\d+))?\)$/i)
|
||||
if (rgbaMatch) {
|
||||
const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1
|
||||
return alpha === 0
|
||||
}
|
||||
|
||||
// 匹配 hsla(h, s, l, a)
|
||||
const hslaMatch = color.match(/^hsla?\(\s*(\d+)(?:deg)?\s*,\s*(\d+)%\s*,\s*(\d+)%(?:\s*,\s*(\d*\.?\d+))?\)$/i)
|
||||
if (hslaMatch) {
|
||||
const alpha = hslaMatch[4] ? parseFloat(hslaMatch[4]) : 1
|
||||
return alpha === 0
|
||||
}
|
||||
|
||||
// 匹配 #RRGGBBAA 或 #RGBA
|
||||
const hexMatch = color.match(/^#([0-9a-f]{8}|[0-9a-f]{4})$/i)
|
||||
if (hexMatch) {
|
||||
const hex = hexMatch[1]
|
||||
const alphaHex = hex.length === 8 ? hex.slice(6, 8) : hex.slice(3, 4).repeat(2)
|
||||
const alpha = parseInt(alphaHex, 16)
|
||||
return alpha === 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.penColor,
|
||||
() => {
|
||||
setLine()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.lineWidth,
|
||||
() => {
|
||||
setLine()
|
||||
}
|
||||
)
|
||||
|
||||
const canvasStyle = computed(() => {
|
||||
const style: CSSProperties = {}
|
||||
if (isDef(props.width)) {
|
||||
style.width = addUnit(props.width)
|
||||
}
|
||||
|
||||
if (isDef(props.height)) {
|
||||
style.height = addUnit(props.height)
|
||||
}
|
||||
|
||||
return `${objToStyle(style)}`
|
||||
})
|
||||
|
||||
const disableScroll = computed(() => props.disableScroll)
|
||||
const enableHistory = computed(() => props.enableHistory)
|
||||
|
||||
const lines = ref<Line[]>([]) // 保存所有线条
|
||||
const redoLines = ref<Line[]>([]) // 保存撤销的线条
|
||||
const currentLine = ref<Line>() // 当前正在绘制的线
|
||||
const currentStep = ref(0) // 当前步骤
|
||||
|
||||
// 添加计算笔画宽度的方法
|
||||
function calculateLineWidth(speed: number): number {
|
||||
if (!props.pressure) return props.lineWidth
|
||||
|
||||
const minSpeed = props.minSpeed || 1.5
|
||||
const limitedSpeed = Math.min(minSpeed * 10, Math.max(minSpeed, speed))
|
||||
const addWidth = ((props.maxWidth - props.minWidth) * (limitedSpeed - minSpeed)) / minSpeed
|
||||
const lineWidth = Math.max(props.maxWidth - addWidth, props.minWidth)
|
||||
return Math.min(lineWidth, props.maxWidth)
|
||||
}
|
||||
|
||||
/* 获取默认笔画宽度 */
|
||||
const getDefaultLineWidth = () => {
|
||||
if (props.pressure) {
|
||||
// 在压感模式下,使用最大和最小宽度的平均值作为默认值
|
||||
return (props.maxWidth + props.minWidth) / 2
|
||||
}
|
||||
return props.lineWidth
|
||||
}
|
||||
|
||||
/* 开始画线 */
|
||||
const startDrawing = (e: any) => {
|
||||
e.preventDefault()
|
||||
drawing.value = true
|
||||
setLine()
|
||||
emit('start', e)
|
||||
|
||||
// 创建新线条,同时保存当前的所有绘制参数
|
||||
const { x, y } = e.touches[0]
|
||||
currentLine.value = {
|
||||
points: [
|
||||
{
|
||||
x,
|
||||
y,
|
||||
t: Date.now() // 使用 t 替换 width
|
||||
}
|
||||
],
|
||||
color: props.penColor,
|
||||
width: getDefaultLineWidth(),
|
||||
backgroundColor: props.backgroundColor,
|
||||
isPressure: props.pressure // 添加笔锋模式标记
|
||||
}
|
||||
|
||||
// 清空重做记录
|
||||
redoLines.value = []
|
||||
draw(e)
|
||||
}
|
||||
|
||||
/* 结束画线 */
|
||||
const stopDrawing = (e: TouchEvent) => {
|
||||
e.preventDefault()
|
||||
drawing.value = false
|
||||
if (currentLine.value) {
|
||||
// 保存完整的线条信息,包括所有点的参数
|
||||
lines.value.push({
|
||||
...currentLine.value,
|
||||
points: currentLine.value.points.map((point) => ({
|
||||
...point,
|
||||
t: point.t,
|
||||
speed: point.speed,
|
||||
distance: point.distance,
|
||||
lineWidth: point.lineWidth,
|
||||
lastX1: point.lastX1,
|
||||
lastY1: point.lastY1,
|
||||
lastX2: point.lastX2,
|
||||
lastY2: point.lastY2,
|
||||
isFirstPoint: point.isFirstPoint
|
||||
}))
|
||||
})
|
||||
currentStep.value = lines.value.length
|
||||
}
|
||||
currentLine.value = undefined
|
||||
const { ctx } = canvasState
|
||||
if (ctx) ctx.beginPath()
|
||||
emit('end', e)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 canvas
|
||||
* @param forceUpdate 是否强制更新
|
||||
*/
|
||||
const initCanvas = (forceUpdate: boolean = false) => {
|
||||
// 如果不是强制更新,且已经初始化过 canvas,则不再重复初始化
|
||||
if (!forceUpdate && canvasState.canvasHeight && canvasState.canvasWidth) {
|
||||
return
|
||||
}
|
||||
getContext().then(() => {
|
||||
const { ctx } = canvasState
|
||||
if (ctx && isDef(props.backgroundColor)) {
|
||||
ctx.setFillStyle(props.backgroundColor)
|
||||
ctx.fillRect(0, 0, canvasState.canvasWidth, canvasState.canvasHeight)
|
||||
ctx.draw()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清空 canvas
|
||||
const clear = () => {
|
||||
lines.value = []
|
||||
redoLines.value = []
|
||||
currentStep.value = 0
|
||||
clearCanvas()
|
||||
emit('clear')
|
||||
}
|
||||
|
||||
// 确认签名
|
||||
const confirmSignature = () => {
|
||||
canvasToImage()
|
||||
}
|
||||
|
||||
//canvas划线
|
||||
const draw = (e: any) => {
|
||||
e.preventDefault()
|
||||
const { ctx } = canvasState
|
||||
|
||||
if (!drawing.value || props.disabled || !ctx) return
|
||||
const { x, y } = e.touches[0]
|
||||
|
||||
const point: Point = {
|
||||
x,
|
||||
y,
|
||||
t: Date.now()
|
||||
}
|
||||
|
||||
if (currentLine.value) {
|
||||
const points = currentLine.value.points
|
||||
const prePoint = points[points.length - 1]
|
||||
|
||||
if (prePoint.t === point.t || (prePoint.x === x && prePoint.y === y)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算点的速度和距离
|
||||
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)
|
||||
|
||||
if (props.pressure) {
|
||||
point.lineWidth = calculateLineWidth(point.speed)
|
||||
// 处理线宽变化率限制
|
||||
if (points.length >= 2) {
|
||||
const prePoint2 = points[points.length - 2]
|
||||
if (prePoint2.lineWidth && prePoint.lineWidth) {
|
||||
const rate = (point.lineWidth - prePoint.lineWidth) / prePoint.lineWidth
|
||||
const maxRate = 0.2 // 最大变化率20%
|
||||
if (Math.abs(rate) > maxRate) {
|
||||
const per = rate > 0 ? maxRate : -maxRate
|
||||
point.lineWidth = prePoint.lineWidth * (1 + per)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
points.push(point)
|
||||
|
||||
// 非笔锋模式直接使用线段连接
|
||||
if (!props.pressure) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(prePoint.x, prePoint.y)
|
||||
ctx.lineTo(point.x, point.y)
|
||||
ctx.stroke()
|
||||
ctx.draw(true)
|
||||
} else if (points.length >= 2) {
|
||||
// 笔锋模式使用贝塞尔曲线
|
||||
drawSmoothLine(prePoint, point)
|
||||
}
|
||||
}
|
||||
|
||||
emit('signing', e)
|
||||
}
|
||||
|
||||
/* 重绘整个画布 */
|
||||
const redrawCanvas = () => {
|
||||
const { ctx } = canvasState
|
||||
if (!ctx) return
|
||||
|
||||
// 清除画布并设置背景
|
||||
if (!isTransparentColor(props.backgroundColor)) {
|
||||
ctx.setFillStyle(props.backgroundColor as string)
|
||||
ctx.fillRect(0, 0, canvasState.canvasWidth, canvasState.canvasHeight)
|
||||
} else {
|
||||
ctx.clearRect(0, 0, canvasState.canvasWidth, canvasState.canvasHeight)
|
||||
}
|
||||
|
||||
// 如果没有线条,只需要清空画布
|
||||
if (lines.value.length === 0) {
|
||||
ctx.draw()
|
||||
return
|
||||
}
|
||||
|
||||
// 收集所有绘制操作,最后一次性 draw
|
||||
lines.value.forEach((line) => {
|
||||
if (!line.points.length) return
|
||||
|
||||
ctx.setStrokeStyle(line.color)
|
||||
ctx.setLineJoin('round')
|
||||
ctx.setLineCap('round')
|
||||
|
||||
if (line.isPressure && props.pressure) {
|
||||
// 笔锋模式的重绘
|
||||
line.points.forEach((point, index) => {
|
||||
if (index === 0) return
|
||||
const prePoint = line.points[index - 1]
|
||||
const dis_x = point.x - prePoint.x
|
||||
const dis_y = point.y - prePoint.y
|
||||
const distance = Math.sqrt(dis_x * dis_x + dis_y * dis_y)
|
||||
|
||||
if (distance <= 2) {
|
||||
point.lastX1 = point.lastX2 = prePoint.x + dis_x * 0.5
|
||||
point.lastY1 = point.lastY2 = prePoint.y + dis_y * 0.5
|
||||
} else {
|
||||
const speed = point.speed || 0
|
||||
const minSpeed = props.minSpeed || 1.5
|
||||
const speedFactor = Math.max(0.1, Math.min(0.9, speed / (minSpeed * 10)))
|
||||
|
||||
point.lastX1 = prePoint.x + dis_x * (0.2 + speedFactor * 0.3)
|
||||
point.lastY1 = prePoint.y + dis_y * (0.2 + speedFactor * 0.3)
|
||||
point.lastX2 = prePoint.x + dis_x * (0.8 - speedFactor * 0.3)
|
||||
point.lastY2 = prePoint.y + dis_y * (0.8 - speedFactor * 0.3)
|
||||
}
|
||||
|
||||
const lineWidth = point.lineWidth || line.width
|
||||
if (typeof prePoint.lastX1 === 'number') {
|
||||
ctx.setLineWidth(lineWidth)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(prePoint.lastX2!, prePoint.lastY2!)
|
||||
ctx.quadraticCurveTo(prePoint.x, prePoint.y, point.lastX1, point.lastY1)
|
||||
ctx.stroke()
|
||||
|
||||
if (!prePoint.isFirstPoint) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(prePoint.lastX1!, prePoint.lastY1!)
|
||||
ctx.quadraticCurveTo(prePoint.x, prePoint.y, prePoint.lastX2!, prePoint.lastY2!)
|
||||
ctx.stroke()
|
||||
}
|
||||
} else {
|
||||
point.isFirstPoint = true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 非笔锋模式的重绘
|
||||
ctx.setLineWidth(line.width)
|
||||
line.points.forEach((point, index) => {
|
||||
if (index === 0) return
|
||||
const prePoint = line.points[index - 1]
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(prePoint.x, prePoint.y)
|
||||
ctx.lineTo(point.x, point.y)
|
||||
ctx.stroke()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 所有线条绘制完成后,一次性更新画布
|
||||
ctx.draw()
|
||||
}
|
||||
|
||||
// 修改撤销功能
|
||||
const revoke = () => {
|
||||
if (!lines.value.length) return
|
||||
const step = Math.min(props.step, lines.value.length)
|
||||
const removedLines = lines.value.splice(lines.value.length - step)
|
||||
redoLines.value.push(...removedLines)
|
||||
currentStep.value = Math.max(0, currentStep.value - step)
|
||||
redrawCanvas()
|
||||
}
|
||||
|
||||
// 修改恢复功能
|
||||
const restore = () => {
|
||||
if (!redoLines.value.length) return
|
||||
const step = Math.min(props.step, redoLines.value.length)
|
||||
const restoredLines = redoLines.value.splice(redoLines.value.length - step)
|
||||
lines.value.push(...restoredLines)
|
||||
currentStep.value = Math.min(lines.value.length, currentStep.value + step)
|
||||
redrawCanvas()
|
||||
}
|
||||
|
||||
// 添加平滑线条绘制方法
|
||||
function drawSmoothLine(prePoint: Point, point: Point) {
|
||||
const { ctx } = canvasState
|
||||
if (!ctx) return
|
||||
|
||||
// 计算两点间距离
|
||||
const dis_x = point.x - prePoint.x
|
||||
const dis_y = point.y - prePoint.y
|
||||
const distance = Math.sqrt(dis_x * dis_x + dis_y * dis_y)
|
||||
|
||||
if (distance <= 2) {
|
||||
// 对于非常近的点,直接使用中点
|
||||
point.lastX1 = point.lastX2 = prePoint.x + dis_x * 0.5
|
||||
point.lastY1 = point.lastY2 = prePoint.y + dis_y * 0.5
|
||||
} else {
|
||||
// 根据点的速度计算控制点的偏移程度
|
||||
const speed = point.speed || 0
|
||||
const minSpeed = props.minSpeed || 1.5
|
||||
const speedFactor = Math.max(0.1, Math.min(0.9, speed / (minSpeed * 10)))
|
||||
|
||||
// 计算控制点
|
||||
point.lastX1 = prePoint.x + dis_x * (0.2 + speedFactor * 0.3)
|
||||
point.lastY1 = prePoint.y + dis_y * (0.2 + speedFactor * 0.3)
|
||||
point.lastX2 = prePoint.x + dis_x * (0.8 - speedFactor * 0.3)
|
||||
point.lastY2 = prePoint.y + dis_y * (0.8 - speedFactor * 0.3)
|
||||
}
|
||||
|
||||
// 计算线宽
|
||||
const lineWidth = point.lineWidth || props.lineWidth
|
||||
|
||||
// 绘制贝塞尔曲线
|
||||
if (typeof prePoint.lastX1 === 'number') {
|
||||
// 设置线宽
|
||||
ctx.setLineWidth(lineWidth)
|
||||
// 绘制第一段曲线
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(prePoint.lastX2!, prePoint.lastY2!)
|
||||
ctx.quadraticCurveTo(prePoint.x, prePoint.y, point.lastX1, point.lastY1)
|
||||
ctx.stroke()
|
||||
|
||||
if (!prePoint.isFirstPoint) {
|
||||
// 绘制连接段曲线
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(prePoint.lastX1!, prePoint.lastY1!)
|
||||
ctx.quadraticCurveTo(prePoint.x, prePoint.y, prePoint.lastX2!, prePoint.lastY2!)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 批量更新绘制内容
|
||||
ctx.draw(true)
|
||||
} else {
|
||||
point.isFirstPoint = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
// #ifdef MP
|
||||
pixelRatio.value = uni.getSystemInfoSync().pixelRatio
|
||||
// #endif
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取canvas上下文
|
||||
*/
|
||||
function getContext() {
|
||||
return new Promise<UniApp.CanvasContext>((resolve) => {
|
||||
const { ctx } = canvasState
|
||||
|
||||
if (ctx) {
|
||||
return resolve(ctx)
|
||||
}
|
||||
// #ifndef MP-WEIXIN
|
||||
getRect(`#${canvasId.value}`, false, proxy).then((canvasRect) => {
|
||||
setcanvasState(canvasRect.width!, canvasRect.height!)
|
||||
canvasState.ctx = uni.createCanvasContext(canvasId.value, proxy)
|
||||
if (canvasState.ctx) {
|
||||
canvasState.ctx.scale(pixelRatio.value, pixelRatio.value)
|
||||
}
|
||||
resolve(canvasState.ctx)
|
||||
})
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
|
||||
getRect(`#${canvasId.value}`, false, proxy, true).then((canvasRect: any) => {
|
||||
if (canvasRect && canvasRect.node && canvasRect.width && canvasRect.height) {
|
||||
const canvasInstance = canvasRect.node
|
||||
canvasState.ctx = canvas2dAdapter(canvasInstance.getContext('2d') as CanvasRenderingContext2D)
|
||||
canvasInstance.width = canvasRect.width * pixelRatio.value
|
||||
canvasInstance.height = canvasRect.height * pixelRatio.value
|
||||
canvasState.ctx.scale(pixelRatio.value, pixelRatio.value)
|
||||
canvas = canvasInstance
|
||||
setcanvasState(canvasRect.width, canvasRect.height)
|
||||
resolve(canvasState.ctx)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 canvasState
|
||||
*/
|
||||
function setcanvasState(width: number, height: number) {
|
||||
canvasState.canvasHeight = height * pixelRatio.value
|
||||
canvasState.canvasWidth = width * pixelRatio.value
|
||||
}
|
||||
|
||||
/* 设置线段 */
|
||||
function setLine() {
|
||||
const { ctx } = canvasState
|
||||
if (ctx) {
|
||||
ctx.setLineWidth(getDefaultLineWidth()) // 使用新的默认宽度
|
||||
ctx.setStrokeStyle(props.penColor)
|
||||
ctx.setLineJoin('round')
|
||||
ctx.setLineCap('round')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* canvas 绘制图片输出成文件类型
|
||||
*/
|
||||
function canvasToImage() {
|
||||
const { fileType, quality, exportScale } = props
|
||||
const { canvasWidth, canvasHeight } = canvasState
|
||||
uni.canvasToTempFilePath(
|
||||
{
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
destWidth: canvasWidth * exportScale,
|
||||
destHeight: canvasHeight * exportScale,
|
||||
fileType,
|
||||
quality,
|
||||
canvasId: canvasId.value,
|
||||
canvas: canvas,
|
||||
success: (res) => {
|
||||
const result: SignatureResult = {
|
||||
tempFilePath: res.tempFilePath,
|
||||
width: (canvasWidth * exportScale) / pixelRatio.value,
|
||||
height: (canvasHeight * exportScale) / pixelRatio.value,
|
||||
success: true
|
||||
}
|
||||
// #ifdef MP-DINGTALK
|
||||
result.tempFilePath = (res as any).filePath
|
||||
// #endif
|
||||
emit('confirm', result)
|
||||
},
|
||||
fail: () => {
|
||||
const result: SignatureResult = {
|
||||
tempFilePath: '',
|
||||
width: (canvasWidth * exportScale) / pixelRatio.value,
|
||||
height: (canvasHeight * exportScale) / pixelRatio.value,
|
||||
success: false
|
||||
}
|
||||
emit('confirm', result)
|
||||
}
|
||||
},
|
||||
proxy
|
||||
)
|
||||
}
|
||||
|
||||
function clearCanvas() {
|
||||
const { canvasWidth, canvasHeight, ctx } = canvasState
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||
if (!isTransparentColor(props.backgroundColor)) {
|
||||
ctx.setFillStyle(props.backgroundColor as string)
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight)
|
||||
}
|
||||
ctx.draw()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose<SignatureExpose>({
|
||||
init: initCanvas,
|
||||
clear,
|
||||
confirm: confirmSignature,
|
||||
restore,
|
||||
revoke
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import './index.scss';
|
||||
</style>
|
||||
Reference in New Issue
Block a user