懒得解释,直接就可以用
> <div class="container">
<CustomNavbar title="签名"></CustomNavbar>
<div class="content">
<div class="orientation-tip">
<i class="fas fa-mobile-alt"></i> 横屏模式下书写体验更佳
</div>
<div class="signature-container">
<div class="canvas-section">
<div class="canvas-wrapper">
<canvas canvas-id="signatureCanvas" id="signatureCanvas" class="signature-canvas"
disable-scroll="true" @touchstart="handleTouchStart" @touchmove="handleTouchMove"
@touchend="handleTouchEnd"></canvas>
<div class="performance-indicator">
FPS: {{ fps }} | 延迟: {{ latency }}ms | 点数: {{ pointCount }}
</div>
<div class="placeholder" v-if="!hasSignature">
<i class="fas fa-pen-nib" style="font-size: 40px; margin-bottom: 10px;"></i>
<div>请在此处签名</div>
<div style="font-size: 14px; margin-top: 10px;">笔画丝滑无偏移</div>
</div>
</div>
<div class="controls">
<button class="btn btn-clear" @click="clearSignature">
<i class="fas fa-eraser"></i> 清除
</button>
<button class="btn btn-undo" @click="undo" :disabled="!canUndo">
<i class="fas fa-undo"></i> 撤销
</button>
<button class="btn btn-save" @click="saveSignature" :disabled="!hasSignature">
<i class="fas fa-save"></i> 保存
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import CustomNavbar from '@/components/custom-navbar.vue'
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
const hasSignature = ref(false)
const canUndo = ref(false)
const signatureDataUrl = ref('')
const penWidth = ref(6)
const fps = ref(0)
const latency = ref(0)
const pointCount = ref(0)
const canvasWidth = ref(0)
const canvasHeight = ref(0)
// 高性能绘图变量
let ctx = null
let isDrawing = false
let paths = []
let currentPath = null
let lastRenderTime = 0
let fpsCounter = 0
let lastFpsUpdate = Date.now()
let lastPoints = []
// 性能优化配置
const CONFIG = {
THROTTLE_DELAY: 4,
USE_BEZIER: true,
BATCH_DRAW: true
}
onMounted(() => {
initCanvas()
startFpsMonitor()
// 监听横竖屏变化
uni.onWindowResize((res) => {
setTimeout(() => {
initCanvasSize()
redrawCanvas()
}, 300)
})
})
onUnmounted(() => {
paths = []
currentPath = null
uni.offWindowResize()
})
// 初始化 Canvas 尺寸
const initCanvasSize = () => {
const systemInfo = uni.getSystemInfoSync()
const isLandscape = systemInfo.windowWidth > systemInfo.windowHeight
if (isLandscape) {
// 横屏模式 - 使用窗口宽度
canvasWidth.value = systemInfo.windowWidth - 50 // 减去padding
canvasHeight.value = systemInfo.windowHeight - 200 // 减去其他元素高度
} else {
// 竖屏模式
canvasWidth.value = systemInfo.windowWidth - 50
canvasHeight.value = 400
}
console.log('Canvas尺寸:', canvasWidth.value, canvasHeight.value)
}
// 初始化 Canvas
const initCanvas = () => {
initCanvasSize()
// 使用nextTick确保DOM更新后再创建canvas上下文
nextTick(() => {
ctx = uni.createCanvasContext('signatureCanvas', this)
// 设置画布实际像素尺寸
const query = uni.createSelectorQuery().in(this)
query.select('#signatureCanvas').boundingClientRect(res => {
if (res) {
console.log('Canvas元素尺寸:', res.width, res.height)
// 设置canvas实际绘制尺寸
ctx.width = res.width
ctx.height = res.height
// 设置高性能绘制参数
ctx.lineWidth = penWidth.value
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
ctx.strokeStyle = '#2c3e50'
// 预绘制空白画布
redrawCanvas()
}
}).exec()
})
}
// 高性能触摸开始
const handleTouchStart = (e) => {
const touch = e.touches[0]
const startTime = Date.now()
isDrawing = true
hasSignature.value = true
// 开始新路径
currentPath = {
points: [{ x: touch.x, y: touch.y, t: startTime }],
color: '#2c3e50',
width: penWidth.value
}
lastPoints = [{ x: touch.x, y: touch.y, t: startTime }]
// 立即开始绘制
ctx.beginPath()
ctx.moveTo(touch.x, touch.y)
ctx.stroke()
ctx.draw(true)
pointCount.value++
}
// 高性能触摸移动
const handleTouchMove = (e) => {
if (!isDrawing || !currentPath) return
const currentTime = Date.now()
// 节流控制
if (currentTime - lastRenderTime < CONFIG.THROTTLE_DELAY) {
return
}
const touch = e.touches[0]
const newPoint = { x: touch.x, y: touch.y, t: currentTime }
// 添加点到当前路径
currentPath.points.push(newPoint)
lastPoints.push(newPoint)
// 保持最近3个点用于贝塞尔计算
if (lastPoints.length > 3) {
lastPoints.shift()
}
// 高性能绘制
if (CONFIG.USE_BEZIER && lastPoints.length >= 3) {
drawBezierCurve(lastPoints)
} else {
drawStraightLine(lastPoints)
}
lastRenderTime = currentTime
pointCount.value = currentPath.points.length
latency.value = currentTime - e.timeStamp
}
// 绘制贝塞尔曲线
const drawBezierCurve = (points) => {
if (points.length < 3) return
const p0 = points[0]
const p1 = points[1]
const p2 = points[2]
const cp1x = p1.x + (p2.x - p0.x) / 4
const cp1y = p1.y + (p2.y - p0.y) / 4
ctx.beginPath()
ctx.moveTo(p1.x, p1.y)
ctx.quadraticCurveTo(cp1x, cp1y, p2.x, p2.y)
ctx.stroke()
ctx.draw(true)
}
// 绘制直线
const drawStraightLine = (points) => {
if (points.length < 2) return
const lastPoint = points[points.length - 2]
const currentPoint = points[points.length - 1]
ctx.beginPath()
ctx.moveTo(lastPoint.x, lastPoint.y)
ctx.lineTo(currentPoint.x, currentPoint.y)
ctx.stroke()
ctx.draw(true)
}
// 触摸结束
const handleTouchEnd = () => {
if (!isDrawing || !currentPath) return
isDrawing = false
if (currentPath.points.length > 1) {
paths.push({ ...currentPath })
canUndo.value = paths.length > 0
}
currentPath = null
lastPoints = []
}
// 清除签名
const clearSignature = () => {
ctx.clearRect(0, 0, 10000, 10000)
ctx.draw(true)
hasSignature.value = false
canUndo.value = false
signatureDataUrl.value = ''
paths = []
pointCount.value = 0
}
// 撤销上一步
const undo = () => {
if (paths.length === 0) return
paths.pop()
canUndo.value = paths.length > 0
hasSignature.value = paths.length > 0
redrawCanvas()
}
// 高性能重绘画布
const redrawCanvas = () => {
ctx.clearRect(0, 0, 10000, 10000)
paths.forEach(path => {
if (path.points.length < 2) return
ctx.lineWidth = path.width
ctx.strokeStyle = path.color
ctx.beginPath()
if (path.points.length === 2) {
ctx.moveTo(path.points[0].x, path.points[0].y)
ctx.lineTo(path.points[1].x, path.points[1].y)
} else {
ctx.moveTo(path.points[0].x, path.points[0].y)
for (let i = 1; i < path.points.length; i++) {
ctx.lineTo(path.points[i].x, path.points[i].y)
}
}
ctx.stroke()
})
ctx.draw(true)
}
// 保存签名
const saveSignature = () => {
if (paths.length === 0) return
uni.showLoading({ title: '生成中...' })
setTimeout(() => {
uni.canvasToTempFilePath({
canvasId: 'signatureCanvas',
quality: 1,
success: (res) => {
convertToBase64(res.tempFilePath).then(base64Data => {
uni.hideLoading()
signatureDataUrl.value = res.tempFilePath
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage && prevPage.$vm) {
prevPage.$vm.onBackWithParams({
data: base64Data
})
}
uni.navigateBack()
}).catch(err => {
uni.hideLoading()
console.error('转换为base64失败:', err)
uni.showToast({
title: '保存失败',
icon: 'none'
})
})
},
fail: (err) => {
uni.hideLoading()
console.error('保存签名失败:', err)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
})
}, 100)
}
// 将图像文件转换为base64
const convertToBase64 = (filePath) => {
return new Promise((resolve, reject) => {
uni.getFileSystemManager().readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
const base64Data = 'data:image/png;base64,' + res.data
resolve(base64Data)
},
fail: (error) => {
reject(error)
}
})
})
}
// FPS监控
const startFpsMonitor = () => {
const updateFps = () => {
fpsCounter++
const now = Date.now()
if (now - lastFpsUpdate >= 1000) {
fps.value = fpsCounter
fpsCounter = 0
lastFpsUpdate = now
}
requestAnimationFrame(updateFps)
}
updateFps()
}
</script>
<style scoped lang="scss">
.container {
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, 0.98);
overflow: hidden;
}
.content {
padding: 25px;
height: calc(100% - 80rpx);
display: flex;
flex-direction: column;
}
.orientation-tip {
background: linear-gradient(to right, #ff7e5f, #feb47b);
color: white;
padding: 12px;
text-align: center;
font-size: 14px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.signature-container {
flex: 1;
display: flex;
flex-direction: column;
}
.canvas-section {
flex: 1;
display: flex;
flex-direction: column;
}
.canvas-wrapper {
position: relative;
flex: 1;
width: 100%;
border: 3px dashed #a0aec0;
border-radius: 12px;
background: #f8fafc;
overflow: hidden;
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
.signature-canvas {
width: 100%;
height: 100%;
background: white;
display: block;
touch-action: none;
}
.placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #a0aec0;
font-size: 20px;
text-align: center;
pointer-events: none;
z-index: 1;
}
.performance-indicator {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
z-index: 2;
}
.controls {
display: flex;
justify-content: space-around;
margin: 20rpx 0;
flex-shrink: 0;
}
.btn {
border-radius: 10rpx;
font-size: 14rpx;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid rgba(238, 238, 238, 0.5);
color: #666;
}
.btn:active {
transform: translateY(2rpx);
}
button {
padding: 0;
background: none !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
line-height: normal !important;
border-radius: 0 !important;
font-size: inherit !important;
color: inherit !important;
&::after {
border: none !important;
}
}
/* 横屏样式优化 */
@media screen and (orientation: landscape) {
.container {
max-width: 100vw;
}
.content {
padding: 15px;
}
.canvas-wrapper {
height: 100%;
min-height: auto;
}
.orientation-tip {
display: none;
}
.controls {
margin-top: 15px;
padding: 0 10px;
}
}
/* 竖屏样式 */
@media screen and (orientation: portrait) {
.canvas-wrapper {
height: 400px;
}
}
/* 防止iOS橡皮筋效果 */
body {
position: fixed;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>