智慧教务系统
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

<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>