You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
758 lines
14 KiB
758 lines
14 KiB
<template>
|
|
<view class="contract-sign-container">
|
|
<!-- 顶部操作栏 -->
|
|
<view class="top-actions">
|
|
<view class="clear-btn" @click="clearSignature">
|
|
<text class="clear-text">清除签名</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 合同信息 -->
|
|
<view class="contract-info">
|
|
<view class="info-card">
|
|
<text class="contract-name">{{contractName}}</text>
|
|
<text class="sign-tip">请在下方签名区域进行手写签名</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 签名区域 -->
|
|
<view class="signature-section">
|
|
<view class="signature-title">
|
|
<text class="title-text">请在此区域签名</text>
|
|
<text class="tip-text">支持手指或触控笔签名</text>
|
|
</view>
|
|
|
|
<!-- Canvas 签名板 -->
|
|
<view class="signature-canvas-container">
|
|
<canvas
|
|
class="signature-canvas"
|
|
canvas-id="signatureCanvas"
|
|
@touchstart="touchStart"
|
|
@touchmove="touchMove"
|
|
@touchend="touchEnd"
|
|
disable-scroll="true">
|
|
</canvas>
|
|
|
|
<!-- 签名提示 -->
|
|
<view v-if="!hasSigned" class="signature-placeholder">
|
|
<text class="placeholder-icon">✍️</text>
|
|
<text class="placeholder-text">请在此处签名</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 签名工具栏 -->
|
|
<view class="signature-tools">
|
|
<view class="tool-group">
|
|
<text class="tool-label">笔迹颜色:</text>
|
|
<view class="color-picker">
|
|
<view v-for="color in penColors"
|
|
:key="color"
|
|
class="color-item"
|
|
:class="currentColor === color ? 'active' : ''"
|
|
:style="{ backgroundColor: color }"
|
|
@click="setPenColor(color)">
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="tool-group">
|
|
<text class="tool-label">笔迹粗细:</text>
|
|
<view class="width-picker">
|
|
<view v-for="width in penWidths"
|
|
:key="width"
|
|
class="width-item"
|
|
:class="currentWidth === width ? 'active' : ''"
|
|
@click="setPenWidth(width)">
|
|
<view class="width-preview" :style="{ width: width + 'rpx', height: width + 'rpx' }"></view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 签名预览 -->
|
|
<view v-if="signatureImageUrl" class="preview-section">
|
|
<view class="preview-title">签名预览</view>
|
|
<view class="preview-image-container">
|
|
<image :src="signatureImageUrl" class="preview-image" mode="aspectFit"></image>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 底部操作按钮 -->
|
|
<view class="footer-actions">
|
|
<button class="action-btn secondary" @click="previewSignature">预览签名</button>
|
|
<button class="action-btn primary"
|
|
@click="submitSignature"
|
|
:disabled="!hasSigned || submitting"
|
|
:loading="submitting">
|
|
{{submitting ? '提交中...' : '确认签订'}}
|
|
</button>
|
|
</view>
|
|
|
|
<!-- 签名预览弹窗 -->
|
|
<view v-if="showPreview" class="preview-modal" @click="closePreview">
|
|
<view class="modal-content" @click.stop>
|
|
<view class="modal-header">
|
|
<text class="modal-title">签名预览</text>
|
|
<view class="modal-close" @click="closePreview">
|
|
<text class="iconfont icon-close"></text>
|
|
</view>
|
|
</view>
|
|
<view class="modal-body">
|
|
<image v-if="signatureImageUrl" :src="signatureImageUrl" class="modal-image" mode="aspectFit"></image>
|
|
<text v-else class="no-signature">暂无签名</text>
|
|
</view>
|
|
<view class="modal-footer">
|
|
<button class="modal-btn secondary" @click="closePreview">取消</button>
|
|
<button class="modal-btn primary" @click="confirmSignature">确认签订</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import apiRoute from '@/common/axios.js'
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
contractId: 0,
|
|
contractName: '',
|
|
canvas: null,
|
|
ctx: null,
|
|
isDrawing: false,
|
|
lastPoint: null,
|
|
hasSigned: false,
|
|
signatureImageUrl: '',
|
|
submitting: false,
|
|
showPreview: false,
|
|
|
|
// 画笔设置
|
|
currentColor: '#000000',
|
|
currentWidth: 6,
|
|
penColors: ['#000000', '#FF0000', '#0000FF', '#008000', '#800080'],
|
|
penWidths: [3, 6, 9, 12],
|
|
|
|
// Canvas 尺寸
|
|
canvasWidth: 0,
|
|
canvasHeight: 0,
|
|
pixelRatio: 1
|
|
}
|
|
},
|
|
onLoad(options) {
|
|
if (options.id) {
|
|
this.contractId = parseInt(options.id)
|
|
}
|
|
if (options.contractName) {
|
|
this.contractName = decodeURIComponent(options.contractName)
|
|
}
|
|
|
|
this.$nextTick(() => {
|
|
this.initCanvas()
|
|
})
|
|
},
|
|
onReady() {
|
|
this.initCanvas()
|
|
},
|
|
methods: {
|
|
// 初始化Canvas
|
|
initCanvas() {
|
|
const query = uni.createSelectorQuery().in(this)
|
|
query.select('.signature-canvas').boundingClientRect((rect) => {
|
|
if (rect) {
|
|
this.canvasWidth = rect.width
|
|
this.canvasHeight = rect.height
|
|
|
|
// 获取设备像素比
|
|
const systemInfo = uni.getSystemInfoSync()
|
|
this.pixelRatio = systemInfo.pixelRatio || 1
|
|
|
|
// 创建Canvas上下文
|
|
this.canvas = uni.createCanvasContext('signatureCanvas', this)
|
|
this.ctx = this.canvas
|
|
|
|
// 设置Canvas尺寸
|
|
this.ctx.scale(this.pixelRatio, this.pixelRatio)
|
|
|
|
// 设置画笔属性
|
|
this.ctx.lineWidth = this.currentWidth
|
|
this.ctx.strokeStyle = this.currentColor
|
|
this.ctx.lineCap = 'round'
|
|
this.ctx.lineJoin = 'round'
|
|
|
|
// 清空Canvas
|
|
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|
this.ctx.draw()
|
|
}
|
|
}).exec()
|
|
},
|
|
|
|
// 触摸开始
|
|
touchStart(e) {
|
|
if (!this.ctx) return
|
|
|
|
this.isDrawing = true
|
|
const touch = e.touches[0]
|
|
this.lastPoint = {
|
|
x: touch.x,
|
|
y: touch.y
|
|
}
|
|
|
|
this.ctx.beginPath()
|
|
this.ctx.moveTo(touch.x, touch.y)
|
|
},
|
|
|
|
// 触摸移动
|
|
touchMove(e) {
|
|
if (!this.ctx || !this.isDrawing) return
|
|
|
|
const touch = e.touches[0]
|
|
const currentPoint = {
|
|
x: touch.x,
|
|
y: touch.y
|
|
}
|
|
|
|
this.ctx.lineTo(currentPoint.x, currentPoint.y)
|
|
this.ctx.stroke()
|
|
this.ctx.draw(true)
|
|
|
|
this.lastPoint = currentPoint
|
|
this.hasSigned = true
|
|
},
|
|
|
|
// 触摸结束
|
|
touchEnd(e) {
|
|
this.isDrawing = false
|
|
this.lastPoint = null
|
|
},
|
|
|
|
// 设置画笔颜色
|
|
setPenColor(color) {
|
|
this.currentColor = color
|
|
if (this.ctx) {
|
|
this.ctx.strokeStyle = color
|
|
}
|
|
},
|
|
|
|
// 设置画笔粗细
|
|
setPenWidth(width) {
|
|
this.currentWidth = width
|
|
if (this.ctx) {
|
|
this.ctx.lineWidth = width
|
|
}
|
|
},
|
|
|
|
// 清除签名
|
|
clearSignature() {
|
|
if (this.ctx) {
|
|
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|
this.ctx.draw()
|
|
this.hasSigned = false
|
|
this.signatureImageUrl = ''
|
|
}
|
|
},
|
|
|
|
// 预览签名
|
|
previewSignature() {
|
|
if (!this.hasSigned) {
|
|
uni.showToast({
|
|
title: '请先进行签名',
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
|
|
this.generateSignatureImage(() => {
|
|
this.showPreview = true
|
|
})
|
|
},
|
|
|
|
// 生成签名图片
|
|
generateSignatureImage(callback) {
|
|
if (!this.canvas) return
|
|
|
|
uni.canvasToTempFilePath({
|
|
canvasId: 'signatureCanvas',
|
|
success: (res) => {
|
|
this.signatureImageUrl = res.tempFilePath
|
|
if (callback) callback()
|
|
},
|
|
fail: (err) => {
|
|
console.error('生成签名图片失败:', err)
|
|
uni.showToast({
|
|
title: '生成签名失败',
|
|
icon: 'none'
|
|
})
|
|
}
|
|
}, this)
|
|
},
|
|
|
|
// 关闭预览
|
|
closePreview() {
|
|
this.showPreview = false
|
|
},
|
|
|
|
// 确认签名
|
|
confirmSignature() {
|
|
this.closePreview()
|
|
this.submitSignature()
|
|
},
|
|
|
|
// 提交签名
|
|
submitSignature() {
|
|
if (!this.hasSigned) {
|
|
uni.showToast({
|
|
title: '请先进行签名',
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
|
|
this.generateSignatureImage(() => {
|
|
this.uploadSignature()
|
|
})
|
|
},
|
|
|
|
// 上传签名
|
|
async uploadSignature() {
|
|
if (!this.signatureImageUrl) {
|
|
uni.showToast({
|
|
title: '签名生成失败,请重试',
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
|
|
this.submitting = true
|
|
|
|
try {
|
|
// 先上传签名图片
|
|
const uploadResult = await this.uploadSignatureFile()
|
|
|
|
if (!uploadResult.success) {
|
|
throw new Error(uploadResult.message || '上传签名文件失败')
|
|
}
|
|
|
|
// 提交签名信息
|
|
const response = await apiRoute.post('/contract/sign', {
|
|
contract_id: this.contractId,
|
|
sign_file: uploadResult.url
|
|
})
|
|
|
|
if (response.data.code === 1) {
|
|
uni.showToast({
|
|
title: '签名提交成功',
|
|
icon: 'success',
|
|
duration: 2000
|
|
})
|
|
|
|
setTimeout(() => {
|
|
// 返回到合同详情页面
|
|
uni.navigateBack({
|
|
delta: 1
|
|
})
|
|
}, 2000)
|
|
} else {
|
|
throw new Error(response.data.msg || '提交签名失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('提交签名失败:', error)
|
|
uni.showToast({
|
|
title: error.message || '网络错误,请稍后重试',
|
|
icon: 'none'
|
|
})
|
|
} finally {
|
|
this.submitting = false
|
|
}
|
|
},
|
|
|
|
// 上传签名文件
|
|
uploadSignatureFile() {
|
|
return new Promise((resolve, reject) => {
|
|
uni.uploadFile({
|
|
url: this.$baseUrl + '/uploadImage',
|
|
filePath: this.signatureImageUrl,
|
|
name: 'file',
|
|
header: {
|
|
'Authorization': uni.getStorageSync('token')
|
|
},
|
|
success: (res) => {
|
|
try {
|
|
const data = JSON.parse(res.data)
|
|
if (data.code === 1) {
|
|
resolve({
|
|
success: true,
|
|
url: data.data.url
|
|
})
|
|
} else {
|
|
resolve({
|
|
success: false,
|
|
message: data.msg || '上传失败'
|
|
})
|
|
}
|
|
} catch (error) {
|
|
resolve({
|
|
success: false,
|
|
message: '上传响应解析失败'
|
|
})
|
|
}
|
|
},
|
|
fail: (error) => {
|
|
resolve({
|
|
success: false,
|
|
message: '上传请求失败'
|
|
})
|
|
}
|
|
})
|
|
})
|
|
},
|
|
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.contract-sign-container {
|
|
min-height: 100vh;
|
|
background-color: #1a1a1a;
|
|
padding-bottom: 120rpx;
|
|
}
|
|
|
|
/* 顶部操作栏 */
|
|
.top-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
padding: 20rpx 32rpx;
|
|
|
|
.clear-btn {
|
|
padding: 12rpx 24rpx;
|
|
background-color: #29d3b4;
|
|
border-radius: 8rpx;
|
|
|
|
.clear-text {
|
|
font-size: 28rpx;
|
|
color: #fff;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 合同信息 */
|
|
.contract-info {
|
|
padding: 20rpx;
|
|
|
|
.info-card {
|
|
background-color: #2a2a2a;
|
|
border-radius: 16rpx;
|
|
padding: 32rpx;
|
|
text-align: center;
|
|
border: 1rpx solid #444;
|
|
|
|
.contract-name {
|
|
font-size: 32rpx;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
display: block;
|
|
margin-bottom: 16rpx;
|
|
}
|
|
|
|
.sign-tip {
|
|
font-size: 26rpx;
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 签名区域 */
|
|
.signature-section {
|
|
padding: 0 20rpx;
|
|
margin-bottom: 40rpx;
|
|
|
|
.signature-title {
|
|
text-align: center;
|
|
margin-bottom: 24rpx;
|
|
|
|
.title-text {
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
display: block;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.tip-text {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
|
|
.signature-canvas-container {
|
|
position: relative;
|
|
background-color: #fff;
|
|
border-radius: 16rpx;
|
|
margin-bottom: 32rpx;
|
|
overflow: hidden;
|
|
border: 2rpx dashed #29d3b4;
|
|
|
|
.signature-canvas {
|
|
width: 100%;
|
|
height: 400rpx;
|
|
display: block;
|
|
}
|
|
|
|
.signature-placeholder {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
pointer-events: none;
|
|
|
|
.placeholder-icon {
|
|
font-size: 60rpx;
|
|
display: block;
|
|
margin-bottom: 16rpx;
|
|
}
|
|
|
|
.placeholder-text {
|
|
font-size: 28rpx;
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 签名工具栏 */
|
|
.signature-tools {
|
|
background-color: #2a2a2a;
|
|
border-radius: 16rpx;
|
|
padding: 24rpx;
|
|
border: 1rpx solid #444;
|
|
|
|
.tool-group {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 24rpx;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.tool-label {
|
|
font-size: 26rpx;
|
|
color: #ccc;
|
|
margin-right: 20rpx;
|
|
min-width: 120rpx;
|
|
}
|
|
}
|
|
|
|
.color-picker {
|
|
display: flex;
|
|
gap: 16rpx;
|
|
|
|
.color-item {
|
|
width: 40rpx;
|
|
height: 40rpx;
|
|
border-radius: 50%;
|
|
border: 3rpx solid transparent;
|
|
cursor: pointer;
|
|
|
|
&.active {
|
|
border-color: #29d3b4;
|
|
}
|
|
}
|
|
}
|
|
|
|
.width-picker {
|
|
display: flex;
|
|
gap: 20rpx;
|
|
|
|
.width-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 50rpx;
|
|
height: 50rpx;
|
|
border-radius: 8rpx;
|
|
border: 2rpx solid #e5e5e5;
|
|
cursor: pointer;
|
|
|
|
&.active {
|
|
border-color: #29d3b4;
|
|
background-color: rgba(41, 211, 180, 0.1);
|
|
}
|
|
|
|
.width-preview {
|
|
background-color: #333;
|
|
border-radius: 50%;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 签名预览 */
|
|
.preview-section {
|
|
padding: 0 20rpx;
|
|
margin-bottom: 40rpx;
|
|
|
|
.preview-title {
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
text-align: center;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.preview-image-container {
|
|
background-color: #2a2a2a;
|
|
border-radius: 16rpx;
|
|
padding: 32rpx;
|
|
border: 1rpx solid #444;
|
|
|
|
.preview-image {
|
|
width: 100%;
|
|
height: 200rpx;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 底部操作按钮 */
|
|
.footer-actions {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: #2a2a2a;
|
|
padding: 24rpx 32rpx;
|
|
border-top: 1rpx solid #444;
|
|
display: flex;
|
|
gap: 24rpx;
|
|
z-index: 100;
|
|
|
|
.action-btn {
|
|
flex: 1;
|
|
height: 88rpx;
|
|
border-radius: 12rpx;
|
|
font-size: 32rpx;
|
|
font-weight: 600;
|
|
border: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
&.primary {
|
|
background-color: #29d3b4;
|
|
color: #fff;
|
|
|
|
&:active:not(:disabled) {
|
|
background-color: #22b39a;
|
|
}
|
|
|
|
&:disabled {
|
|
background-color: #666;
|
|
color: #999;
|
|
}
|
|
}
|
|
|
|
&.secondary {
|
|
background-color: #444;
|
|
color: #ccc;
|
|
|
|
&:active {
|
|
background-color: #555;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 预览弹窗 */
|
|
.preview-modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
|
|
.modal-content {
|
|
background-color: #fff;
|
|
border-radius: 20rpx;
|
|
width: 80%;
|
|
max-height: 70%;
|
|
overflow: hidden;
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 32rpx;
|
|
border-bottom: 1rpx solid #e5e5e5;
|
|
|
|
.modal-title {
|
|
font-size: 32rpx;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.modal-close {
|
|
width: 48rpx;
|
|
height: 48rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
.iconfont {
|
|
font-size: 32rpx;
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 32rpx;
|
|
text-align: center;
|
|
|
|
.modal-image {
|
|
width: 100%;
|
|
height: 300rpx;
|
|
}
|
|
|
|
.no-signature {
|
|
font-size: 28rpx;
|
|
color: #999;
|
|
padding: 60rpx 0;
|
|
}
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
gap: 24rpx;
|
|
padding: 32rpx;
|
|
border-top: 1rpx solid #e5e5e5;
|
|
|
|
.modal-btn {
|
|
flex: 1;
|
|
height: 72rpx;
|
|
border-radius: 12rpx;
|
|
font-size: 28rpx;
|
|
font-weight: 600;
|
|
border: none;
|
|
|
|
&.primary {
|
|
background-color: #29d3b4;
|
|
color: #fff;
|
|
}
|
|
|
|
&.secondary {
|
|
background-color: #f5f5f5;
|
|
color: #666;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|