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

864 lines
18 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" v-if="isFormField && fieldName">{{fieldName}}</text>
<text class="contract-name" v-else>{{contractName}}</text>
<text class="sign-tip" v-if="isFormField">请为此字段进行手写签名</text>
<text class="sign-tip" v-else>请在下方签名区域进行手写签名</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 ? '提交中...' : (isFormField ? '确认签名' : '确认签订')}}
</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 '@/api/apiRoute.js'
import { uploadFile } from '@/common/util.js';
export default {
data() {
return {
contractId: 0,
contractName: '',
fieldName: '', // 字段名称
fieldPlaceholder: '', // 字段占位符
isFormField: false, // 是否来自表单字段
isStaff: false, // 是否为员工端
personnelId: 0, // 员工ID
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)
this.isFormField = false
}
if (options.contract_id) {
// 新的表单字段签名方式或员工端签名方式
this.contractId = parseInt(options.contract_id)
this.isFormField = !options.isStaff // 员工端直接签名,不是表单字段
}
// 员工端模式
if (options.isStaff === 'true') {
this.isStaff = true
this.personnelId = parseInt(options.personnel_id) || 0
}
if (options.contractName) {
this.contractName = decodeURIComponent(options.contractName)
}
if (options.field_name) {
this.fieldName = decodeURIComponent(options.field_name)
}
if (options.field_placeholder) {
this.fieldPlaceholder = decodeURIComponent(options.field_placeholder)
}
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
// 设置画笔属性
this.ctx.lineWidth = this.currentWidth
this.ctx.strokeStyle = this.currentColor
this.ctx.lineCap = 'round'
this.ctx.lineJoin = 'round'
// 清空Canvas并设置背景
this.ctx.fillStyle = '#ffffff'
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
this.ctx.draw()
console.log('Canvas初始化完成:', {
width: this.canvasWidth,
height: this.canvasHeight,
pixelRatio: this.pixelRatio
})
}
}).exec()
},
// 触摸开始
touchStart(e) {
if (!this.ctx) {
console.log('Canvas未初始化')
return
}
// 阻止默认行为
e.preventDefault && e.preventDefault()
this.isDrawing = true
const touch = e.touches[0] || e.changedTouches[0]
// UniApp Canvas使用相对坐标
const x = touch.x || touch.clientX || 0
const y = touch.y || touch.clientY || 0
this.lastPoint = { x, y }
console.log('开始绘制:', { x, y, touch })
// 重新设置画笔属性(确保生效)
this.ctx.lineWidth = this.currentWidth
this.ctx.strokeStyle = this.currentColor
this.ctx.lineCap = 'round'
this.ctx.lineJoin = 'round'
this.ctx.beginPath()
this.ctx.moveTo(x, y)
},
// 触摸移动
touchMove(e) {
if (!this.ctx || !this.isDrawing) return
// 阻止默认行为
e.preventDefault && e.preventDefault()
const touch = e.touches[0] || e.changedTouches[0]
// UniApp Canvas使用相对坐标
const x = touch.x || touch.clientX || 0
const y = touch.y || touch.clientY || 0
const currentPoint = { x, y }
// 绘制线条
this.ctx.lineTo(currentPoint.x, currentPoint.y)
this.ctx.stroke()
this.ctx.draw(true)
this.lastPoint = currentPoint
this.hasSigned = true
},
// 触摸结束
touchEnd(e) {
if (this.isDrawing) {
console.log('绘制结束,已签名状态:', this.hasSigned)
}
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) {
// 清除Canvas并重新设置背景
this.ctx.fillStyle = '#ffffff'
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
this.ctx.draw()
// 重置状态
this.hasSigned = false
this.signatureImageUrl = ''
this.isDrawing = false
this.lastPoint = null
console.log('签名已清除')
}
},
// 预览签名
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(() => {
if (this.isFormField || this.isStaff) {
// 表单字段签名或员工端合同签署 - 返回签名数据
this.returnSignatureData()
} else {
// 原有合同签署 - 直接提交
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.signStudentContract({
contract_sign_id: this.contractId,
signature_image: uploadResult.url
})
if (response.code === 1) {
uni.showToast({
title: '签名提交成功',
icon: 'success',
duration: 2000
})
setTimeout(() => {
// 返回到合同详情页面
uni.navigateBack({
delta: 1
})
}, 2000)
} else {
throw new Error(response.msg || '提交签名失败')
}
} catch (error) {
console.error('提交签名失败:', error)
uni.showToast({
title: error.message || '网络错误,请稍后重试',
icon: 'none'
})
} finally {
this.submitting = false
}
},
// 返回签名数据给表单页面
async returnSignatureData() {
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 pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage) {
// 将签名数据传递给前一个页面,使用服务器返回的URL
if (!prevPage.data) {
prevPage.data = {}
}
prevPage.data.newSignature = {
fieldPlaceholder: this.fieldPlaceholder,
signatureData: uploadResult.url // 使用服务器URL而不是临时文件路径
}
}
uni.showToast({
title: this.isStaff ? '签名完成,请填写其他信息' : '签名完成',
icon: 'success',
duration: 1500
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('上传签名失败:', error)
uni.showToast({
title: error.message || '上传签名失败,请重试',
icon: 'none'
})
} finally {
this.submitting = false
}
},
// 上传签名文件
async uploadSignatureFile() {
return new Promise((resolve, reject) => {
uploadFile(
this.signatureImageUrl,
(fileData) => {
resolve({ success: true, url: fileData.url });
},
(err) => {
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>