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

578 lines
13 KiB

<template>
<view class="contract-form-container">
<!-- 自定义导航栏 -->
<view class="navbar_section">
<view class="navbar_back" @click="goBack">
<text class="back_icon"></text>
</view>
<view class="navbar_title">合同签署</view>
<view class="navbar_action"></view>
</view>
<!-- 合同信息 -->
<view class="contract-info">
<view class="info-card">
<text class="contract-name">{{ contractName }}</text>
<text class="contract-type">{{ contractInfo.contract_type || '学员课程协议' }}</text>
</view>
</view>
<!-- 表单区域 -->
<view class="form-section" v-if="formFields.length > 0">
<view class="section-title">合同信息填写</view>
<view class="form-content">
<view v-for="(field, index) in formFields" :key="field.id || index" class="form-field">
<!-- 普通输入字段 -->
<view v-if="field.data_type === 'user_input'" class="input-field">
<view class="field-label">
<text class="label-text">{{ field.name }}</text>
<text v-if="field.is_required" class="required-mark">*</text>
</view>
<!-- 文本输入 -->
<input
v-if="field.field_type === 'text'"
v-model="formData[field.placeholder]"
:placeholder="field.default_value || `请输入${field.name}`"
class="form-input"
/>
<!-- 日期输入 -->
<picker
v-if="field.field_type === 'date'"
mode="date"
:value="formData[field.placeholder] || getCurrentDate()"
@change="onDateChange($event, field.placeholder)"
class="form-picker">
<view class="picker-display">
{{ formData[field.placeholder] || getCurrentDate() }}
</view>
</picker>
<!-- 数字输入 -->
<input
v-if="field.field_type === 'number'"
v-model="formData[field.placeholder]"
type="number"
:placeholder="field.default_value || `请输入${field.name}`"
class="form-input"
/>
</view>
<!-- 签名字段 -->
<view v-if="field.data_type === 'signature'" class="signature-field">
<view class="field-label">
<text class="label-text">{{ field.name }}</text>
<text v-if="field.is_required" class="required-mark">*</text>
</view>
<view class="signature-container">
<view v-if="!signatures[field.placeholder]" class="signature-placeholder" @click="goToSignature(field)">
<text class="placeholder-icon">✍️</text>
<text class="placeholder-text">点击进行签名</text>
</view>
<view v-else class="signature-preview" @click="goToSignature(field)">
<image :src="signatures[field.placeholder]" class="signature-image" mode="aspectFit"></image>
<text class="signature-tip">点击重新签名</text>
</view>
</view>
</view>
<!-- 显示字段 -->
<view v-if="field.data_type === 'database' || field.data_type === 'system'" class="display-field">
<view class="field-label">
<text class="label-text">{{ field.name }}</text>
</view>
<view class="display-value">{{ field.default_value || '自动填充' }}</view>
</view>
</view>
</view>
</view>
<!-- 合同条款(如果有) -->
<view class="terms-section" v-if="contractInfo.contract_content">
<view class="section-title">合同条款</view>
<view class="terms-content">
<text class="terms-text">{{ contractInfo.contract_content }}</text>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="footer-actions">
<button class="action-btn secondary" @click="goBack">取消</button>
<button class="action-btn primary"
@click="submitContract"
:disabled="!isFormValid || submitting"
:loading="submitting">
{{ submitting ? '提交中...' : '确认签署' }}
</button>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
contractId: 0,
studentId: 0,
contractName: '',
contractInfo: {},
formFields: [],
formData: {},
signatures: {},
submitting: false,
loading: false
}
},
computed: {
isFormValid() {
// 检查必填字段是否都已填写
for (let field of this.formFields) {
if (field.is_required) {
if (field.data_type === 'signature') {
if (!this.signatures[field.placeholder]) {
return false
}
} else if (field.data_type === 'user_input') {
if (!this.formData[field.placeholder] || this.formData[field.placeholder].trim() === '') {
return false
}
}
}
}
return true
}
},
onLoad(options) {
this.contractId = parseInt(options.contract_id) || 0
this.studentId = parseInt(options.student_id) || 0
this.contractName = options.contractName ? decodeURIComponent(options.contractName) : '合同签署'
if (this.contractId && this.studentId) {
this.loadSignForm()
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
})
}
},
onShow() {
// 从签名页面返回时,检查是否有新的签名数据
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage.data && currentPage.data.newSignature) {
const signData = currentPage.data.newSignature
this.signatures[signData.fieldPlaceholder] = signData.signatureData
// 清理临时数据
currentPage.data.newSignature = null
}
},
methods: {
goBack() {
uni.navigateBack()
},
async loadSignForm() {
this.loading = true
try {
const response = await apiRoute.getStudentContractSignForm({
contract_id: this.contractId,
student_id: this.studentId
})
if (response.code === 1) {
this.contractInfo = response.data
this.formFields = response.data.form_fields || []
// 初始化表单数据
this.formFields.forEach(field => {
if (field.data_type === 'user_input' && field.default_value) {
this.$set(this.formData, field.placeholder, field.default_value)
}
})
console.log('表单配置加载成功:', this.formFields)
} else {
console.error('API返回错误:', response.msg)
uni.showToast({
title: response.msg || '获取表单配置失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取表单配置失败:', error)
uni.showToast({
title: '获取表单配置失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
getCurrentDate() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
onDateChange(event, fieldPlaceholder) {
this.$set(this.formData, fieldPlaceholder, event.detail.value)
},
goToSignature(field) {
uni.navigateTo({
url: `/pages-common/contract/contract_sign?contract_id=${this.contractId}&field_name=${encodeURIComponent(field.name)}&field_placeholder=${encodeURIComponent(field.placeholder)}`
})
},
async submitContract() {
if (!this.isFormValid) {
uni.showToast({
title: '请填写完整信息并完成签名',
icon: 'none'
})
return
}
this.submitting = true
try {
// 准备提交数据
const submitData = {
contract_id: this.contractId,
student_id: this.studentId,
form_data: { ...this.formData },
signature_image: ''
}
// 添加签名数据到form_data中
for (let field of this.formFields) {
if (field.data_type === 'signature' && this.signatures[field.placeholder]) {
submitData.form_data[field.placeholder] = this.signatures[field.placeholder]
// 如果是主要签名,设置为signature_image
if (!submitData.signature_image) {
submitData.signature_image = this.signatures[field.placeholder]
}
}
}
console.log('提交合同数据:', submitData)
const response = await apiRoute.signStudentContract(submitData)
if (response.code === 1) {
uni.showToast({
title: '合同签署成功',
icon: 'success',
duration: 2000
})
setTimeout(() => {
// 返回合同列表页面
uni.navigateBack({
delta: 2
})
}, 2000)
} else {
throw new Error(response.msg || '合同签署失败')
}
} catch (error) {
console.error('提交合同失败:', error)
uni.showToast({
title: error.message || '网络错误,请稍后重试',
icon: 'none'
})
} finally {
this.submitting = false
}
}
}
}
</script>
<style lang="less" scoped>
.contract-form-container {
min-height: 100vh;
background: #f8f9fa;
padding-bottom: 120rpx;
}
// 自定义导航栏
.navbar_section {
display: flex;
justify-content: space-between;
align-items: center;
background: #29D3B4;
padding: 40rpx 32rpx 20rpx;
// 小程序端适配状态栏
// #ifdef MP-WEIXIN
padding-top: 80rpx;
// #endif
.navbar_back {
width: 60rpx;
.back_icon {
color: #fff;
font-size: 40rpx;
font-weight: 600;
}
}
.navbar_title {
color: #fff;
font-size: 32rpx;
font-weight: 600;
}
.navbar_action {
width: 60rpx;
}
}
// 合同信息
.contract-info {
padding: 20rpx;
.info-card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
text-align: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.contract-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.contract-type {
font-size: 24rpx;
color: #666;
}
}
}
// 表单区域
.form-section {
margin: 20rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
padding: 24rpx 32rpx 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.form-content {
padding: 24rpx 32rpx 32rpx;
.form-field {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.field-label {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.label-text {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.required-mark {
color: #ff4757;
font-size: 26rpx;
margin-left: 4rpx;
}
}
.form-input {
width: 100%;
height: 80rpx;
background: #f8f9fa;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
box-sizing: border-box;
}
.form-picker {
width: 100%;
height: 80rpx;
background: #f8f9fa;
border-radius: 8rpx;
display: flex;
align-items: center;
.picker-display {
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
flex: 1;
}
}
.display-value {
height: 80rpx;
background: #f0f0f0;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #666;
display: flex;
align-items: center;
}
}
}
}
// 签名字段样式
.signature-field {
.signature-container {
background: #f8f9fa;
border-radius: 8rpx;
min-height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx dashed #ddd;
.signature-placeholder {
text-align: center;
color: #999;
.placeholder-icon {
font-size: 40rpx;
display: block;
margin-bottom: 8rpx;
}
.placeholder-text {
font-size: 24rpx;
}
}
.signature-preview {
width: 100%;
text-align: center;
.signature-image {
width: 200rpx;
height: 80rpx;
margin-bottom: 8rpx;
}
.signature-tip {
font-size: 22rpx;
color: #666;
}
}
}
}
// 合同条款
.terms-section {
margin: 20rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
padding: 24rpx 32rpx 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.terms-content {
padding: 24rpx 32rpx 32rpx;
.terms-text {
font-size: 24rpx;
color: #666;
line-height: 1.6;
white-space: pre-line;
}
}
}
// 底部操作按钮
.footer-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 24rpx 32rpx;
border-top: 1rpx solid #f0f0f0;
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: #29D3B4;
color: #fff;
&:active:not(:disabled) {
background: #26c6a0;
}
&:disabled {
background: #ccc;
color: #999;
}
}
&.secondary {
background: #f8f9fa;
color: #666;
&:active {
background: #e9ecef;
}
}
}
}
</style>