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

1302 lines
32 KiB

<!--合同签署表单页面-->
<template>
<view class="main_box">
<!-- 自定义导航栏 -->
<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_section" v-if="contractInfo">
<view class="info_header">
<view class="contract_name">{{ contractInfo.contract_name }}</view>
<view class="contract_type">{{ contractInfo.contract_type }}</view>
</view>
</view>
<!-- 合同内容 -->
<view class="contract_content_section" v-if="contractContent">
<view class="content_header">
<view class="content_title">合同内容</view>
<view class="content_expand_btn" @click="toggleContentExpand">
{{ contentExpanded ? '收起' : '展开' }}
</view>
</view>
<view class="content_body" :class="{ expanded: contentExpanded }">
<view v-html="renderContractContentWithFields" class="content_text"></view>
</view>
</view>
<!-- 表单内容 -->
<view class="form_section" v-if="!loading">
<view class="form_title">请填写以下信息</view>
<view class="form_content">
<view
v-for="(field, index) in filteredFormFields"
:key="index"
:class="['form_field',getFieldClass(field)]"
>
<view class="field_label">
{{ field.name }}
<text v-if="field.is_required" class="required_mark">*</text>
<view v-if="isStaff" class="party_indicator">
<text :class="['party_tag',getPartyClass(field)]">{{ getPartyLabel(field) }}</text>
</view>
</view>
<!-- 手写签名字段 -->
<view v-if="field.data_type === 'signature'" class="field_signature">
<view
class="signature_button"
@click="goToSignature(field)"
:class="formData[field.placeholder] ? 'signed' : ''"
>
<text class="signature_icon">✒️</text>
<text class="signature_text">
{{ formData[field.placeholder] ? '已签名' : '手写签名' }}
</text>
</view>
<view v-if="formData[field.placeholder]" class="signature_preview">
<image :src="formData[field.placeholder]" mode="aspectFit" />
</view>
</view>
<!-- 签名图片上传字段 -->
<view v-else-if="field.data_type === 'sign_img'" class="field_sign_img">
<view
class="sign_img_button"
@click="chooseSignImage(field)"
:class="formData[field.placeholder] ? 'uploaded' : ''"
>
<text class="sign_img_icon">📷</text>
<text class="sign_img_text">
{{ formData[field.placeholder] ? '已上传' : '上传签名图片' }}
</text>
</view>
<view v-if="formData[field.placeholder]" class="sign_img_preview">
<image :src="formData[field.placeholder]" mode="aspectFit" />
</view>
</view>
<!-- 文本输入框 -->
<view v-else-if="field.field_type === 'text'" class="field_input">
<input
type="text"
:placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]"
:disabled="!canEditField(field)"
:class="!canEditField(field) ? 'disabled' : ''"
/>
</view>
<!-- 日期选择器 -->
<view v-else-if="field.field_type === 'date'" class="field_input">
<picker
mode="date"
:value="formData[field.placeholder]"
@change="handleDateChange(field.placeholder, $event)"
:disabled="field.data_type !== 'user_input'"
>
<input
type="text"
:placeholder="field.placeholder || '请选择' + field.name"
:value="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''"
readonly
/>
</picker>
</view>
<!-- 数字输入框 -->
<view v-else-if="field.field_type === 'number'" class="field_input">
<input
type="number"
:placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]"
:disabled="!canEditField(field)"
:class="!canEditField(field) ? 'disabled' : ''"
/>
</view>
<!-- 文本域 -->
<view v-else-if="field.field_type === 'textarea'" class="field_textarea">
<textarea
:placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]"
:disabled="!canEditField(field)"
:class="!canEditField(field) ? 'disabled' : ''"
></textarea>
</view>
<!-- 提示信息 -->
<view v-if="field.data_type !== 'user_input' && field.data_type !== 'signature' && field.data_type !== 'sign_img'" class="field_hint">
{{ getFieldHint(field) }}
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading_section">
<view class="loading_spinner"></view>
<view class="loading_text">加载表单配置中...</view>
</view>
<!-- 提交按钮 -->
<view class="submit_section" v-if="!loading">
<!-- 当status=2且sign_file为空时,显示确认生成合同和修改合同按钮 -->
<view v-if="contractInfo && contractInfo.status == 2 && !contractInfo.sign_file" class="button_group">
<button
class="uni-button generate-btn"
:loading="generating"
@click="confirmGenerateContract"
>
{{ generating ? '生成中...' : '确认生成合同' }}
</button>
<button
class="uni-button modify-btn"
@click="submitContract"
>
修改合同
</button>
</view>
<!-- 当status=2且sign_file有值时,显示下载合同按钮 -->
<button
v-else-if="contractInfo && contractInfo.status == 3 && contractInfo.sign_file"
class="uni-button download-btn"
@click="downloadContract"
>
下载合同
</button>
<!-- 其他情况显示原有的提交签署按钮 -->
<button
v-else
class="uni-button submit-btn"
:loading="submitting"
@click="submitContract"
>
{{ submitting ? '提交中...' : '提交签署' }}
</button>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
import { Api_url,img_domian } from '@/common/config.js'
export default {
data() {
return {
contractId: 0,
studentId: 0,
contractSignId: 0, // 新增:合同签署记录ID
contractName: '',
contractInfo: null,
contractContent: '',
contentExpanded: false,
formFields: [],
formData: {},
loading: true,
submitting: false,
generating: false, // 生成合同状态
// 角色和权限控制
userInfo: null,
userRole: 'student', // 默认为学生角色
isStaffMode: false
}
},
computed: {
// 检查当前用户是否为员工
isStaff() {
return this.userRole === 'staff' || this.isStaffMode
},
// 根据角色过滤表单字段
filteredFormFields() {
if (!this.formFields || this.formFields.length === 0) {
return []
}
// 员工可以填写所有字段
if (this.isStaff) {
return this.formFields
}
// 学生只能填写乙方字段
return this.formFields.filter(field => {
const signParty = field.sign_party || ''
return signParty === 'second_party' || signParty === 'party_b' || signParty === '乙方'
})
},
// 渲染合同内容,替换占位符为实际值
renderContractContent() {
if (!this.contractContent) return ''
let content = this.contractContent
// 将表单数据替换到合同内容中的占位符
Object.keys(this.formData).forEach(key => {
const value = this.formData[key] || ''
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g')
content = content.replace(regex, value)
})
return content
},
// 根据字段配置渲染合同内容,支持用户输入和电子签名字段
renderContractContentWithFields() {
if (!this.contractContent) return ''
let content = this.contractContent
// 遍历表单字段配置,根据数据类型处理占位符
this.formFields.forEach(field => {
const placeholder = field.placeholder || field.name
const regex = new RegExp(`\\{\\{${placeholder}\\}\\}`, 'g')
// 根据数据类型渲染不同的内容
let replacementContent = ''
switch (field.data_type) {
case 'user_input':
// 用户输入字段:显示输入框或当前值
const inputValue = this.formData[placeholder] || field.default_value || ''
if (inputValue) {
replacementContent = `<span style="border-bottom: 1px solid #333; padding: 2px 8px; min-width: 80px; display: inline-block;">${inputValue}</span>`
} else {
replacementContent = `<span style="border-bottom: 1px solid #ccc; padding: 2px 8px; min-width: 80px; display: inline-block; color: #999;">待填写</span>`
}
break
case 'signature':
// 电子签名字段:显示签名状态或占位符
const signatureValue = this.formData[placeholder]
if (signatureValue) {
replacementContent = `<span style="display: inline-block; border: 1px solid #27ae60; padding: 4px 8px; background: rgba(39, 174, 96, 0.1); color: #27ae60; border-radius: 4px;">✓ 已签名</span>`
} else {
replacementContent = `<span style="display: inline-block; border: 1px dashed #29D3B4; padding: 4px 8px; background: rgba(41, 211, 180, 0.05); color: #29D3B4; border-radius: 4px;">✒️ 待签名</span>`
}
break
case 'sign_img':
// 签名图片字段:显示上传状态
const signImgValue = this.formData[placeholder]
if (signImgValue) {
replacementContent = `<span style="display: inline-block; border: 1px solid #67c23a; padding: 4px 8px; background: rgba(103, 194, 58, 0.1); color: #67c23a; border-radius: 4px;">✓ 已上传</span>`
} else {
replacementContent = `<span style="display: inline-block; border: 1px dashed #409eff; padding: 4px 8px; background: rgba(64, 158, 255, 0.05); color: #409eff; border-radius: 4px;">📷 待上传</span>`
}
break
case 'database':
case 'system':
default:
// 数据库字段和系统字段:直接显示值
const defaultValue = this.formData[placeholder] || field.default_value || ''
if (defaultValue) {
replacementContent = `<span style="font-weight: 500;">${defaultValue}</span>`
} else {
replacementContent = `<span style="color: #999; font-style: italic;">系统自动获取</span>`
}
break
}
content = content.replace(regex, replacementContent)
})
return content
}
},
onLoad(options) {
this.contractId = parseInt(options.contract_id) || 0
this.studentId = parseInt(options.student_id) || 0
this.contractName = decodeURIComponent(options.contract_name || '')
// 新增支持合同签署记录ID用于已存在的签署记录
this.contractSignId = parseInt(options.contract_sign_id) || 0
// 角色检测
this.userRole = options.user_role || 'student'
this.isStaffMode = options.user_role === 'staff'
// 获取用户信息进行角色验证
this.getUserInfo()
if (this.contractId && this.studentId) {
this.loadSignForm()
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
},
onShow() {
// 从签名页面返回时,检查是否有新的签名数据
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage.data && currentPage.data.newSignature) {
const { fieldPlaceholder, signatureData } = currentPage.data.newSignature
this.formData[fieldPlaceholder] = signatureData
// 清除临时数据
delete currentPage.data.newSignature
}
},
methods: {
goBack() {
uni.navigateBack()
},
// 获取用户信息
getUserInfo() {
try {
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
this.userInfo = JSON.parse(userInfo)
// 如果缓存的用户类型与URL参数不符,以URL参数为准
if (this.userInfo.user_type && this.userRole === 'student') {
this.userRole = this.userInfo.user_type === 'staff' ? 'staff' : 'student'
this.isStaffMode = this.userRole === 'staff'
}
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
},
// 检查字段是否可编辑
canEditField(field) {
// 非用户输入类型的字段都不可编辑
if (field.data_type !== 'user_input') {
return false
}
// 员工可以编辑所有用户输入字段
if (this.isStaff) {
return true
}
// 学生只能编辑乙方字段
const signParty = field.sign_party || ''
return signParty === 'second_party' || signParty === 'party_b' || signParty === '乙方'
},
// 获取字段样式类
getFieldClass(field) {
const classes = []
if (this.isStaff) {
const signParty = field.sign_party || ''
if (signParty === 'first_party' || signParty === 'party_a' || signParty === '甲方') {
classes.push('first_party_field')
} else if (signParty === 'second_party' || signParty === 'party_b' || signParty === '乙方') {
classes.push('second_party_field')
}
}
if (!this.canEditField(field)) {
classes.push('readonly_field')
}
return classes.join(' ')
},
// 获取甲乙方标签样式类
getPartyClass(field) {
const signParty = field.sign_party || ''
if (signParty === 'first_party' || signParty === 'party_a' || signParty === '甲方') {
return 'party_first'
} else if (signParty === 'second_party' || signParty === 'party_b' || signParty === '乙方') {
return 'party_second'
}
return 'party_unknown'
},
// 获取甲乙方标签文本
getPartyLabel(field) {
const signParty = field.sign_party || ''
if (signParty === 'first_party' || signParty === 'party_a' || signParty === '甲方') {
return '甲方'
} else if (signParty === 'second_party' || signParty === 'party_b' || signParty === '乙方') {
return '乙方'
}
return '未知'
},
async loadSignForm() {
this.loading = true
try {
console.log('加载签署表单:', {
contractId: this.contractId,
studentId: this.studentId,
contractSignId: this.contractSignId
})
let response;
// 优先使用contract_sign_id获取签署表单(支持按签署关系区分字段)
if (this.contractSignId && this.contractSignId > 0) {
console.log('使用签署记录ID获取表单配置')
response = await apiRoute.getStudentContractSignFormBySignId({
contract_sign_id: this.contractSignId,
student_id: this.studentId
})
} else {
console.log('使用合同模板ID获取表单配置(兼容模式)')
// 兼容旧版本:使用contract_id获取签署表单
response = await apiRoute.getStudentContractSignForm({
contract_id: this.contractId,
student_id: this.studentId
})
}
if (response.code === 1) {
const data = response.data
this.contractInfo = {
contract_sign_id: data.contract_sign_id,
contract_name: data.contract_name,
contract_type: data.contract_type,
status: data.status,
sign_file: data.sign_file
}
this.contractContent = data.contract_content || ''
this.formFields = data.form_fields || []
// 初始化表单数据
this.initFormData()
console.log('表单配置加载成功:', this.formFields)
} else {
uni.showToast({
title: response.msg || '获取表单配置失败',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
} catch (error) {
console.error('获取表单配置失败:', error)
uni.showToast({
title: '获取表单配置失败',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} finally {
this.loading = false
}
},
initFormData() {
const data = {}
this.formFields.forEach(field => {
const key = field.placeholder || field.name
// 根据数据类型设置初始值
if (field.data_type === 'database' || field.data_type === 'system') {
data[key] = field.default_value || ''
} else {
data[key] = field.default_value || ''
}
})
this.formData = data
},
handleDateChange(fieldKey, event) {
this.formData[fieldKey] = event.detail.value
},
toggleContentExpand() {
this.contentExpanded = !this.contentExpanded
},
goToSignature(field) {
// 跳转到签名页面
const fieldPlaceholder = field.placeholder || field.name
uni.navigateTo({
url: `/pages-common/contract/contract_sign?field_name=${encodeURIComponent(field.name)}&field_placeholder=${encodeURIComponent(fieldPlaceholder)}&contract_id=${this.contractId}`
})
},
chooseSignImage(field) {
// 选择签名图片
const fieldPlaceholder = field.placeholder || field.name
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
// 上传图片到服务器
this.uploadSignImage(tempFilePath, fieldPlaceholder)
},
fail: (err) => {
console.error('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none'
})
}
})
},
async uploadSignImage(tempFilePath, fieldPlaceholder) {
// 显示上传进度
uni.showLoading({
title: '上传中...',
mask: true
})
try {
// 上传图片到服务器
const uploadResult = await new Promise((resolve, reject) => {
uni.uploadFile({
url: Api_url + '/file/image',
filePath: tempFilePath,
name: 'file',
header: {
'Authorization': uni.getStorageSync('token') || ''
},
success: (uploadRes) => {
try {
const result = JSON.parse(uploadRes.data)
if (result.code === 1) {
resolve(result.data)
} else {
reject(new Error(result.msg || '上传失败'))
}
} catch (e) {
reject(new Error('上传响应解析失败'))
}
},
fail: (err) => {
reject(new Error('上传请求失败'))
}
})
})
// 上传成功,使用服务器返回的URL
this.formData[fieldPlaceholder] = uploadResult.url || uploadResult.path || tempFilePath
uni.hideLoading()
uni.showToast({
title: '上传成功',
icon: 'success'
})
console.log('签名图片上传成功:', uploadResult)
} catch (error) {
console.error('签名图片上传失败:', error)
// 上传失败时使用本地临时路径(仅供预览)
this.formData[fieldPlaceholder] = tempFilePath
uni.hideLoading()
uni.showToast({
title: '上传失败,使用本地图片',
icon: 'none',
duration: 2000
})
}
},
getFieldHint(field) {
switch (field.data_type) {
case 'database':
return '此信息将从系统数据库自动获取'
case 'system':
return '此信息将由系统自动生成'
default:
return ''
}
},
// 获取签名图片(从表单数据中提取签名字段)
getSignatureImage() {
// 查找签名类型的字段
const signatureFields = this.formFields.filter(field =>
field.data_type === 'signature' || field.data_type === 'sign_img'
)
// 返回第一个签名字段的值,如果没有则返回空字符串
if (signatureFields.length > 0) {
const signatureField = signatureFields[0]
const fieldKey = signatureField.placeholder || signatureField.name
return this.formData[fieldKey] || ''
}
return ''
},
validateForm() {
// 验证必填字段
for (const field of this.formFields) {
if (field.is_required) {
const key = field.placeholder || field.name
const value = this.formData[key]
if (!value || value.toString().trim() === '') {
uni.showToast({
title: `请填写${field.name}`,
icon: 'none'
})
return false
}
}
}
return true
},
async submitContract() {
if (!this.validateForm()) {
return
}
this.submitting = true
try {
console.log('提交合同签署:', {
contractId: this.contractId,
studentId: this.studentId,
formData: this.formData
})
// 根据角色提交签署数据
let response
if (this.isStaff) {
// 员工端签署
response = await apiRoute.signStaffContract({
contract_id: this.contractId,
contract_sign_id: this.contractSignId,
form_data: this.formData,
signature_image: this.getSignatureImage()
})
} else {
// 学生端签署
response = await apiRoute.signStudentContract({
contract_id: this.contractId,
contract_sign_id: this.contractSignId,
student_id: this.studentId,
form_data: this.formData,
signature_image: this.getSignatureImage()
})
}
if (response.code === 1) {
uni.showToast({
title: '合同签署成功',
icon: 'success',
duration: 2000
})
setTimeout(() => {
// 返回合同列表并刷新
uni.navigateBack()
}, 2000)
} else {
uni.showToast({
title: response.msg || '合同签署失败',
icon: 'none'
})
}
} catch (error) {
console.error('合同签署失败:', error)
uni.showToast({
title: '合同签署失败',
icon: 'none'
})
} finally {
this.submitting = false
}
},
// 确认生成合同
async confirmGenerateContract() {
this.generating = true
try {
console.log('确认生成合同:', {
contractSignId: this.contractSignId
})
const response = await apiRoute.confirmGenerateContract({
contract_sign_id: this.contractSignId
})
if (response.code === 1) {
uni.showToast({
title: '合同生成成功',
icon: 'success',
duration: 2000
})
// 重新加载表单数据以更新合同状态
setTimeout(() => {
this.loadSignForm()
}, 2000)
} else {
uni.showToast({
title: response.msg || '合同生成失败',
icon: 'none'
})
}
} catch (error) {
console.error('合同生成失败:', error)
uni.showToast({
title: '合同生成失败',
icon: 'none'
})
} finally {
this.generating = false
}
},
// 下载合同
downloadContract() {
if (!this.contractInfo || !this.contractInfo.sign_file) {
uni.showToast({
title: '合同文件不存在',
icon: 'none'
})
return
}
// 下载合同文件
uni.showLoading({
title: '准备下载...',
mask: true
})
// 构建完整的文件URL
const fileUrl = this.contractInfo.sign_file.startsWith('http')
? this.contractInfo.sign_file
: img_domian + this.contractInfo.sign_file
// 下载文件
uni.downloadFile({
url: fileUrl,
success: (res) => {
uni.hideLoading()
if (res.statusCode === 200) {
// 保存到相册或打开文件
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
uni.showToast({
title: '下载成功',
icon: 'success'
})
// 打开文件
uni.openDocument({
filePath: saveRes.savedFilePath,
success: () => {
console.log('打开文档成功')
},
fail: (err) => {
console.error('打开文档失败:', err)
}
})
},
fail: (err) => {
console.error('保存文件失败:', err)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
})
} else {
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
},
fail: (err) => {
uni.hideLoading()
console.error('下载失败:', err)
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
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_section {
background: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 24rpx;
.info_header {
.contract_name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.contract_type {
font-size: 24rpx;
color: #666;
}
}
}
// 合同内容区域
.contract_content_section {
background: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 24rpx;
.content_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.content_title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.content_expand_btn {
font-size: 24rpx;
color: #29D3B4;
padding: 8rpx 16rpx;
border: 1rpx solid #29D3B4;
border-radius: 8rpx;
background: rgba(41, 211, 180, 0.05);
}
}
.content_body {
max-height: 200rpx;
overflow: hidden;
transition: max-height 0.3s ease;
&.expanded {
max-height: none;
}
.content_text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
word-break: break-all;
}
}
}
// 表单区域
.form_section {
margin: 20rpx;
.form_title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
padding: 0 8rpx;
}
.form_content {
.form_field {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.field_label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
.required_mark {
color: #e74c3c;
margin-left: 4rpx;
}
}
.field_input {
input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
background: #fff;
&::placeholder {
color: #999;
}
&.disabled {
background: #f5f5f5;
color: #666;
}
}
}
.field_textarea {
textarea {
width: 100%;
min-height: 120rpx;
padding: 20rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
background: #fff;
&::placeholder {
color: #999;
}
&.disabled {
background: #f5f5f5;
color: #666;
}
}
}
.field_signature {
.signature_button {
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
border: 2rpx dashed #29D3B4;
border-radius: 12rpx;
background: rgba(41, 211, 180, 0.05);
&.signed {
border-color: #27ae60;
background: rgba(39, 174, 96, 0.05);
}
.signature_icon {
font-size: 32rpx;
margin-right: 8rpx;
}
.signature_text {
font-size: 28rpx;
color: #29D3B4;
.signed & {
color: #27ae60;
}
}
}
.signature_preview {
margin-top: 16rpx;
image {
width: 100%;
height: 120rpx;
border-radius: 8rpx;
border: 1rpx solid #e0e0e0;
}
}
}
.field_sign_img {
.sign_img_button {
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
border: 2rpx dashed #409eff;
border-radius: 12rpx;
background: rgba(64, 158, 255, 0.05);
&.uploaded {
border-color: #67c23a;
background: rgba(103, 194, 58, 0.05);
}
.sign_img_icon {
font-size: 32rpx;
margin-right: 8rpx;
}
.sign_img_text {
font-size: 28rpx;
color: #409eff;
.uploaded & {
color: #67c23a;
}
}
}
.sign_img_preview {
margin-top: 16rpx;
image {
width: 100%;
height: 120rpx;
border-radius: 8rpx;
border: 1rpx solid #e0e0e0;
}
}
}
.field_hint {
margin-top: 8rpx;
font-size: 22rpx;
color: #999;
font-style: italic;
}
}
}
}
// 加载状态
.loading_section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 32rpx;
.loading_spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-left: 4rpx solid #29D3B4;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
.loading_text {
font-size: 28rpx;
color: #666;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// 甲乙方标识和字段样式
.party_indicator {
display: inline-flex;
align-items: center;
margin-left: 12rpx;
.party_tag {
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 6rpx;
font-weight: 500;
&.party_first {
background: rgba(255, 152, 0, 0.1);
color: #ff9800;
border: 1rpx solid rgba(255, 152, 0, 0.3);
}
&.party_second {
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
border: 1rpx solid rgba(76, 175, 80, 0.3);
}
&.party_unknown {
background: rgba(158, 158, 158, 0.1);
color: #9e9e9e;
border: 1rpx solid rgba(158, 158, 158, 0.3);
}
}
}
// 字段样式区分
.form_field {
&.first_party_field {
border-left: 4rpx solid #ff9800;
background: linear-gradient(90deg, rgba(255, 152, 0, 0.02) 0%, transparent 100%);
.field_label {
color: #e65100;
}
}
&.second_party_field {
border-left: 4rpx solid #4caf50;
background: linear-gradient(90deg, rgba(76, 175, 80, 0.02) 0%, transparent 100%);
.field_label {
color: #2e7d32;
}
}
&.readonly_field {
background: #f8f9fa;
.field_label {
color: #6c757d;
}
input, textarea {
background: #f8f9fa !important;
color: #6c757d !important;
}
}
}
// 提交按钮
.submit_section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10rpx);
padding: 20rpx 32rpx 40rpx;
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
z-index: 999;
// 小程序端适配底部安全区域
// #ifdef MP-WEIXIN
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
// #endif
// 为了确保完全不透明,添加伪元素背景
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #fff;
z-index: -1;
}
// 按钮组布局
.button_group {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20rpx;
}
// UniApp原生按钮样式
.uni-button {
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
outline: none;
&.generate-btn {
flex: 1;
background: linear-gradient(135deg, #FF6B35 0%, #F7931E 100%);
color: #fff;
}
&.modify-btn {
flex: 1;
background: linear-gradient(135deg, #29D3B4 0%, #1BA89A 100%);
color: #fff;
}
&.download-btn {
width: 100%;
background: linear-gradient(135deg, #52C41A 0%, #389E0D 100%);
color: #fff;
}
&.submit-btn {
width: 100%;
background: linear-gradient(135deg, #FF6B35 0%, #F7931E 100%);
color: #fff;
}
// 按钮按下效果
&:active {
opacity: 0.8;
transform: scale(0.98);
}
// 加载状态
&[loading] {
opacity: 0.7;
}
}
}
</style>