|
|
@ -10,20 +10,21 @@ |
|
|
<div class="sign-content" v-loading="loading"> |
|
|
<div class="sign-content" v-loading="loading"> |
|
|
<!-- 合同基础信息 --> |
|
|
<!-- 合同基础信息 --> |
|
|
<div class="contract-header" v-if="contractData"> |
|
|
<div class="contract-header" v-if="contractData"> |
|
|
<h2>{{ contractData.contract?.contract_name || '合同签署' }}</h2> |
|
|
<h2>{{ contractData.contract_id_name || '合同签署' }}</h2> |
|
|
<div class="contract-meta"> |
|
|
<div class="contract-meta"> |
|
|
<el-tag :type="getStatusType(contractData.status)" size="large"> |
|
|
<el-tag :type="getStatusType(contractData.status)" size="large"> |
|
|
{{ getStatusLabel(contractData.status) }} |
|
|
{{ getStatusLabel(contractData.status) }} |
|
|
</el-tag> |
|
|
</el-tag> |
|
|
<span class="contract-type">{{ contractData.contract?.contract_type }}</span> |
|
|
<span class="contract-type">{{ contractData.contract_type }}</span> |
|
|
<span class="create-time">创建时间:{{ formatDate(contractData.created_at) }}</span> |
|
|
<span class="create-time">创建时间:{{ formatDate(contractData.created_at) }}</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 主要内容区域:左右布局 --> |
|
|
<!-- 主要内容区域:左右布局 --> |
|
|
<div class="contract-main"> |
|
|
<div class="contract-main"> |
|
|
<!-- 左侧:合同内容 --> |
|
|
<div class="contract-content-wrapper"> |
|
|
<div class="contract-content-panel"> |
|
|
<!-- 左侧:合同内容 --> |
|
|
|
|
|
<div class="contract-content-panel"> |
|
|
<div class="panel-header"> |
|
|
<div class="panel-header"> |
|
|
<h3>合同内容</h3> |
|
|
<h3>合同内容</h3> |
|
|
<div class="panel-actions"> |
|
|
<div class="panel-actions"> |
|
|
@ -107,8 +108,8 @@ |
|
|
<el-tag size="small" type="info">系统</el-tag> |
|
|
<el-tag size="small" type="info">系统</el-tag> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 签名图片字段 --> |
|
|
<!-- 电子签名字段 (手写签名) --> |
|
|
<div v-else-if="field.data_type === 'signature' || field.data_type === 'sign_img'" class="signature-field"> |
|
|
<div v-else-if="field.data_type === 'signature'" class="signature-field"> |
|
|
<div v-if="field.display_value" class="signature-preview"> |
|
|
<div v-if="field.display_value" class="signature-preview"> |
|
|
<el-image |
|
|
<el-image |
|
|
:src="field.display_value" |
|
|
:src="field.display_value" |
|
|
@ -116,9 +117,41 @@ |
|
|
:preview-src-list="[field.display_value]" |
|
|
:preview-src-list="[field.display_value]" |
|
|
fit="contain" |
|
|
fit="contain" |
|
|
/> |
|
|
/> |
|
|
<el-tag size="small" type="warning">签名</el-tag> |
|
|
<el-tag size="small" type="warning">电子签名</el-tag> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-else class="signature-action"> |
|
|
|
|
|
<el-button size="small" type="primary" @click="openSignatureDialog(field)"> |
|
|
|
|
|
进行电子签名 |
|
|
|
|
|
</el-button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<!-- 签名图片字段 (印章/图片上传) --> |
|
|
|
|
|
<div v-else-if="field.data_type === 'sign_img'" class="signature-field"> |
|
|
|
|
|
<div v-if="field.display_value" class="signature-preview"> |
|
|
|
|
|
<el-image |
|
|
|
|
|
:src="field.display_value" |
|
|
|
|
|
style="width: 80px; height: 40px;" |
|
|
|
|
|
:preview-src-list="[field.display_value]" |
|
|
|
|
|
fit="contain" |
|
|
|
|
|
/> |
|
|
|
|
|
<el-tag size="small" type="success">印章图片</el-tag> |
|
|
|
|
|
<el-button size="small" @click="clearSignatureImage(field)" style="margin-left: 8px;"> |
|
|
|
|
|
清除 |
|
|
|
|
|
</el-button> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-else class="signature-upload"> |
|
|
|
|
|
<input |
|
|
|
|
|
type="file" |
|
|
|
|
|
:ref="(el) => fileInputs[field.placeholder] = el" |
|
|
|
|
|
accept="image/*" |
|
|
|
|
|
style="display: none" |
|
|
|
|
|
@change="(e) => handleSignatureFileSelect(e, field)" |
|
|
|
|
|
/> |
|
|
|
|
|
<el-button size="small" type="success" @click="triggerFileUpload(field)"> |
|
|
|
|
|
上传印章/签名图片 |
|
|
|
|
|
</el-button> |
|
|
</div> |
|
|
</div> |
|
|
<span v-else class="no-signature">未签名</span> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 字段信息 --> |
|
|
<!-- 字段信息 --> |
|
|
@ -139,6 +172,7 @@ |
|
|
<el-empty description="暂无可编辑字段" /> |
|
|
<el-empty description="暂无可编辑字段" /> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 签名区域 --> |
|
|
<!-- 签名区域 --> |
|
|
@ -206,6 +240,13 @@ |
|
|
<template #footer> |
|
|
<template #footer> |
|
|
<span class="dialog-footer"> |
|
|
<span class="dialog-footer"> |
|
|
<el-button @click="showDialog = false">取消</el-button> |
|
|
<el-button @click="showDialog = false">取消</el-button> |
|
|
|
|
|
<el-button |
|
|
|
|
|
type="success" |
|
|
|
|
|
:loading="saving" |
|
|
|
|
|
@click="handleSaveFields" |
|
|
|
|
|
> |
|
|
|
|
|
保存字段 |
|
|
|
|
|
</el-button> |
|
|
<el-button |
|
|
<el-button |
|
|
v-if="contractData?.status === 1" |
|
|
v-if="contractData?.status === 1" |
|
|
type="primary" |
|
|
type="primary" |
|
|
@ -224,7 +265,8 @@ |
|
|
import { ref, computed, watch, nextTick } from 'vue' |
|
|
import { ref, computed, watch, nextTick } from 'vue' |
|
|
import { ElMessage } from 'element-plus' |
|
|
import { ElMessage } from 'element-plus' |
|
|
import { Upload, Check } from '@element-plus/icons-vue' |
|
|
import { Upload, Check } from '@element-plus/icons-vue' |
|
|
import { updateContractSignStatus, getContractSignInfo } from '@/app/api/contract_sign' |
|
|
import { updateContractSignStatus, getContractSignInfo, editContractSign } from '@/app/api/contract_sign' |
|
|
|
|
|
import { uploadImage } from '@/utils/common' |
|
|
|
|
|
|
|
|
interface Props { |
|
|
interface Props { |
|
|
show: boolean |
|
|
show: boolean |
|
|
@ -233,7 +275,7 @@ interface Props { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const props = defineProps<Props>() |
|
|
const props = defineProps<Props>() |
|
|
const emit = defineEmits(['update:show', 'success']) |
|
|
const emit = defineEmits(['update:show', 'success', 'fieldsSaved']) |
|
|
|
|
|
|
|
|
// 响应式数据 |
|
|
// 响应式数据 |
|
|
const showDialog = computed({ |
|
|
const showDialog = computed({ |
|
|
@ -243,6 +285,7 @@ const showDialog = computed({ |
|
|
|
|
|
|
|
|
const loading = ref(false) |
|
|
const loading = ref(false) |
|
|
const signing = ref(false) |
|
|
const signing = ref(false) |
|
|
|
|
|
const saving = ref(false) |
|
|
const signMethod = ref('type') // 'type' | 'upload' |
|
|
const signMethod = ref('type') // 'type' | 'upload' |
|
|
const signatureImage = ref('') |
|
|
const signatureImage = ref('') |
|
|
const isDrawing = ref(false) |
|
|
const isDrawing = ref(false) |
|
|
@ -251,6 +294,8 @@ const currentStroke = ref<{x: number, y: number}[]>([]) |
|
|
const signatureCanvas = ref<HTMLCanvasElement>() |
|
|
const signatureCanvas = ref<HTMLCanvasElement>() |
|
|
const formRef = ref() |
|
|
const formRef = ref() |
|
|
const contentDisplay = ref<HTMLElement>() |
|
|
const contentDisplay = ref<HTMLElement>() |
|
|
|
|
|
const fileInputs = ref<Record<string, HTMLInputElement>>({}) |
|
|
|
|
|
const currentUploadField = ref<any>(null) |
|
|
|
|
|
|
|
|
// 完整的合同签署信息 |
|
|
// 完整的合同签署信息 |
|
|
const fullContractData = ref<any>(null) |
|
|
const fullContractData = ref<any>(null) |
|
|
@ -456,8 +501,8 @@ const getFieldTypeLabel = (dataType: string) => { |
|
|
'user_input': '用户输入', |
|
|
'user_input': '用户输入', |
|
|
'database': '数据库', |
|
|
'database': '数据库', |
|
|
'system': '系统函数', |
|
|
'system': '系统函数', |
|
|
'signature': '手写签名', |
|
|
'signature': '电子签名', |
|
|
'sign_img': '签名图片' |
|
|
'sign_img': '印章图片' |
|
|
} |
|
|
} |
|
|
return labels[dataType as keyof typeof labels] || dataType |
|
|
return labels[dataType as keyof typeof labels] || dataType |
|
|
} |
|
|
} |
|
|
@ -468,7 +513,7 @@ const getFieldTypeColor = (dataType: string) => { |
|
|
'database': 'success', |
|
|
'database': 'success', |
|
|
'system': 'info', |
|
|
'system': 'info', |
|
|
'signature': 'warning', |
|
|
'signature': 'warning', |
|
|
'sign_img': 'warning' |
|
|
'sign_img': 'success' |
|
|
} |
|
|
} |
|
|
return colors[dataType as keyof typeof colors] || 'info' |
|
|
return colors[dataType as keyof typeof colors] || 'info' |
|
|
} |
|
|
} |
|
|
@ -541,25 +586,55 @@ const refreshContent = () => { |
|
|
// 初始化表单数据和规则 |
|
|
// 初始化表单数据和规则 |
|
|
const initFormData = () => { |
|
|
const initFormData = () => { |
|
|
// 使用完整合同数据中的字段配置 |
|
|
// 使用完整合同数据中的字段配置 |
|
|
if (!fullContractData.value?.field_configs) return |
|
|
if (!fullContractData.value?.field_configs) { |
|
|
|
|
|
console.warn('fullContractData或field_configs为空,无法初始化表单') |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log('初始化表单数据 - fullContractData:', fullContractData.value) |
|
|
|
|
|
console.log('初始化表单数据 - fill_data:', fullContractData.value.fill_data) |
|
|
|
|
|
|
|
|
formData.value = {} |
|
|
formData.value = {} |
|
|
formRules.value = {} |
|
|
formRules.value = {} |
|
|
|
|
|
|
|
|
|
|
|
// 优先使用fill_data中的数据,其次使用field_configs中的value |
|
|
|
|
|
const savedFillData = fullContractData.value.fill_data || {} |
|
|
|
|
|
|
|
|
fullContractData.value.field_configs.forEach((field: any) => { |
|
|
fullContractData.value.field_configs.forEach((field: any) => { |
|
|
// 初始化表单数据 |
|
|
// 对于所有字段类型,优先使用已保存的数据 |
|
|
if (field.is_editable && field.data_type === 'user_input') { |
|
|
if (field.is_editable) { |
|
|
formData.value[field.placeholder] = field.value || field.default_value || '' |
|
|
// 数据优先级:fill_data > field.value > field.default_value > '' |
|
|
|
|
|
const savedValue = savedFillData[field.placeholder] |
|
|
|
|
|
const fieldValue = field.value |
|
|
|
|
|
const defaultValue = field.default_value |
|
|
|
|
|
|
|
|
|
|
|
if (savedValue !== undefined && savedValue !== null && savedValue !== '') { |
|
|
|
|
|
// 使用已保存的数据 |
|
|
|
|
|
formData.value[field.placeholder] = savedValue |
|
|
|
|
|
} else if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { |
|
|
|
|
|
// 使用字段配置中的值 |
|
|
|
|
|
formData.value[field.placeholder] = fieldValue |
|
|
|
|
|
} else if (defaultValue !== undefined && defaultValue !== null && defaultValue !== '') { |
|
|
|
|
|
// 使用默认值 |
|
|
|
|
|
formData.value[field.placeholder] = defaultValue |
|
|
|
|
|
} else { |
|
|
|
|
|
// 设为空字符串 |
|
|
|
|
|
formData.value[field.placeholder] = '' |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log(`字段 ${field.placeholder} 初始化值:`, formData.value[field.placeholder]) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 设置验证规则 |
|
|
// 设置验证规则 - 只对用户输入字段 |
|
|
if (field.is_required && field.is_editable) { |
|
|
if (field.is_required && field.is_editable && field.data_type === 'user_input') { |
|
|
formRules.value[field.placeholder] = [ |
|
|
formRules.value[field.placeholder] = [ |
|
|
{ required: true, message: `请填写${field.placeholder}`, trigger: 'blur' } |
|
|
{ required: true, message: `请填写${field.placeholder}`, trigger: 'blur' } |
|
|
] |
|
|
] |
|
|
} |
|
|
} |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
console.log('表单数据初始化完成:', formData.value) |
|
|
|
|
|
|
|
|
// 初始化处理后的内容 |
|
|
// 初始化处理后的内容 |
|
|
updateProcessedContent() |
|
|
updateProcessedContent() |
|
|
} |
|
|
} |
|
|
@ -635,6 +710,10 @@ const loadFullContractData = async () => { |
|
|
if (response.data) { |
|
|
if (response.data) { |
|
|
fullContractData.value = response.data |
|
|
fullContractData.value = response.data |
|
|
|
|
|
|
|
|
|
|
|
// 数据加载完成后初始化表单数据 |
|
|
|
|
|
await nextTick() |
|
|
|
|
|
initFormData() |
|
|
|
|
|
|
|
|
// 更新处理后的合同内容 |
|
|
// 更新处理后的合同内容 |
|
|
updateProcessedContent() |
|
|
updateProcessedContent() |
|
|
} else { |
|
|
} else { |
|
|
@ -647,6 +726,186 @@ const loadFullContractData = async () => { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 处理签名字段的方法 |
|
|
|
|
|
const openSignatureDialog = (field: any) => { |
|
|
|
|
|
// 使用弹窗底部的签名区域来进行签名 |
|
|
|
|
|
ElMessage.info(`请在下方签名区域为"${field.placeholder}"进行电子签名`) |
|
|
|
|
|
|
|
|
|
|
|
// 滚动到签名区域 |
|
|
|
|
|
const signatureSection = document.querySelector('.signature-section') |
|
|
|
|
|
if (signatureSection) { |
|
|
|
|
|
signatureSection.scrollIntoView({ behavior: 'smooth' }) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 触发文件选择 |
|
|
|
|
|
const triggerFileUpload = (field: any) => { |
|
|
|
|
|
currentUploadField.value = field |
|
|
|
|
|
const input = fileInputs.value[field.placeholder] |
|
|
|
|
|
if (input) { |
|
|
|
|
|
input.click() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 处理签名文件选择 |
|
|
|
|
|
const handleSignatureFileSelect = async (event: Event, field: any) => { |
|
|
|
|
|
const input = event.target as HTMLInputElement |
|
|
|
|
|
const file = input.files?.[0] |
|
|
|
|
|
|
|
|
|
|
|
if (!file) return |
|
|
|
|
|
|
|
|
|
|
|
// 验证文件类型 |
|
|
|
|
|
if (!file.type.startsWith('image/')) { |
|
|
|
|
|
ElMessage.error('请选择图片文件') |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 验证文件大小 (5MB) |
|
|
|
|
|
if (file.size > 5 * 1024 * 1024) { |
|
|
|
|
|
ElMessage.error('图片大小不能超过5MB') |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
// 显示上传中状态 |
|
|
|
|
|
ElMessage.info('正在上传图片...') |
|
|
|
|
|
|
|
|
|
|
|
// 使用便捷的上传方法 |
|
|
|
|
|
const result = await uploadImage(file, { |
|
|
|
|
|
onProgress: (percent) => { |
|
|
|
|
|
console.log(`上传进度: ${percent}%`) |
|
|
|
|
|
}, |
|
|
|
|
|
onError: (error) => { |
|
|
|
|
|
ElMessage.error(error || '上传失败') |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// 处理上传结果 |
|
|
|
|
|
if (result.success && result.url) { |
|
|
|
|
|
// 保存图片URL到字段配置中 |
|
|
|
|
|
if (fullContractData.value?.field_configs) { |
|
|
|
|
|
const fieldConfig = fullContractData.value.field_configs.find((f: any) => f.placeholder === field.placeholder) |
|
|
|
|
|
if (fieldConfig) { |
|
|
|
|
|
console.log('保存图片URL:', result.url, '到字段:', field.placeholder) |
|
|
|
|
|
console.log('字段配置前:', JSON.stringify(fieldConfig)) |
|
|
|
|
|
|
|
|
|
|
|
// 强制设置display_value |
|
|
|
|
|
fieldConfig.display_value = result.url |
|
|
|
|
|
// 也保存到formData中,用于提交 |
|
|
|
|
|
formData.value[field.placeholder] = result.url |
|
|
|
|
|
|
|
|
|
|
|
console.log('字段配置后:', JSON.stringify(fieldConfig)) |
|
|
|
|
|
console.log('formData:', JSON.stringify(formData.value)) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 强制更新视图 |
|
|
|
|
|
await nextTick() |
|
|
|
|
|
|
|
|
|
|
|
// 更新合同内容显示 |
|
|
|
|
|
updateProcessedContent() |
|
|
|
|
|
|
|
|
|
|
|
ElMessage.success('印章图片上传成功') |
|
|
|
|
|
} else { |
|
|
|
|
|
ElMessage.error(result.error || '上传失败') |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
console.error('上传失败:', error) |
|
|
|
|
|
ElMessage.error('上传失败,请重试') |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 清空文件输入 |
|
|
|
|
|
input.value = '' |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 清除签名图片 |
|
|
|
|
|
const clearSignatureImage = (field: any) => { |
|
|
|
|
|
if (fullContractData.value?.field_configs) { |
|
|
|
|
|
const fieldConfig = fullContractData.value.field_configs.find((f: any) => f.placeholder === field.placeholder) |
|
|
|
|
|
if (fieldConfig) { |
|
|
|
|
|
fieldConfig.display_value = '' |
|
|
|
|
|
// 从formData中移除 |
|
|
|
|
|
delete formData.value[field.placeholder] |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
ElMessage.success('签名图片已清除') |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 保存字段信息 |
|
|
|
|
|
const handleSaveFields = async () => { |
|
|
|
|
|
if (!props.contractData?.id) { |
|
|
|
|
|
ElMessage.error('缺少必要参数') |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
saving.value = true |
|
|
|
|
|
|
|
|
|
|
|
// 获取服务器端最新的fill_data作为基础 |
|
|
|
|
|
const serverFillData = fullContractData.value?.fill_data || {} |
|
|
|
|
|
|
|
|
|
|
|
// 合并数据:服务器数据 + 当前表单数据 + 字段配置中的数据 |
|
|
|
|
|
const fillData = { ...serverFillData } |
|
|
|
|
|
|
|
|
|
|
|
// 添加当前表单数据 |
|
|
|
|
|
Object.keys(formData.value).forEach(key => { |
|
|
|
|
|
if (formData.value[key] !== undefined && formData.value[key] !== null && formData.value[key] !== '') { |
|
|
|
|
|
fillData[key] = formData.value[key] |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// 收集所有字段数据(包括签名字段) |
|
|
|
|
|
if (fullContractData.value?.field_configs) { |
|
|
|
|
|
fullContractData.value.field_configs.forEach((field: any) => { |
|
|
|
|
|
if (field.data_type === 'sign_img' || field.data_type === 'signature') { |
|
|
|
|
|
// 对于签名字段,优先使用formData中的值,其次使用display_value |
|
|
|
|
|
const formValue = formData.value[field.placeholder] |
|
|
|
|
|
const displayValue = field.display_value |
|
|
|
|
|
|
|
|
|
|
|
if (formValue && formValue !== '') { |
|
|
|
|
|
fillData[field.placeholder] = formValue |
|
|
|
|
|
} else if (displayValue && displayValue !== '') { |
|
|
|
|
|
fillData[field.placeholder] = displayValue |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log('保存字段数据 - 原始服务器数据:', serverFillData) |
|
|
|
|
|
console.log('保存字段数据 - 表单数据:', formData.value) |
|
|
|
|
|
console.log('保存字段数据 - 最终合并数据:', fillData) |
|
|
|
|
|
|
|
|
|
|
|
const params = { |
|
|
|
|
|
id: props.contractData.id, |
|
|
|
|
|
contract_id: props.contractData.contract_id, |
|
|
|
|
|
personnel_id: props.personnelId, |
|
|
|
|
|
fill_data: JSON.stringify(fillData), |
|
|
|
|
|
// 保持其他字段不变 |
|
|
|
|
|
status: props.contractData.status, |
|
|
|
|
|
signature_image: props.contractData.signature_image, |
|
|
|
|
|
sign_time: props.contractData.sign_time, |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log('保存字段数据:', params) |
|
|
|
|
|
|
|
|
|
|
|
// 调用编辑API保存字段信息 |
|
|
|
|
|
await editContractSign(params) |
|
|
|
|
|
ElMessage.success('字段保存成功') |
|
|
|
|
|
|
|
|
|
|
|
// 保存成功后重新加载数据,确保前端状态同步 |
|
|
|
|
|
await loadFullContractData() |
|
|
|
|
|
|
|
|
|
|
|
emit('fieldsSaved', fillData) // 发出保存成功事件 |
|
|
|
|
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
console.error('保存字段失败:', error) |
|
|
|
|
|
ElMessage.error('保存字段失败,请重试') |
|
|
|
|
|
} finally { |
|
|
|
|
|
saving.value = false |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 监听弹窗显示状态 |
|
|
// 监听弹窗显示状态 |
|
|
watch(() => props.show, (visible) => { |
|
|
watch(() => props.show, (visible) => { |
|
|
if (visible) { |
|
|
if (visible) { |
|
|
@ -657,7 +916,7 @@ watch(() => props.show, (visible) => { |
|
|
currentStroke.value = [] |
|
|
currentStroke.value = [] |
|
|
fullContractData.value = null |
|
|
fullContractData.value = null |
|
|
|
|
|
|
|
|
// 加载完整合同数据 |
|
|
// 加载完整合同数据 (会在加载完成后自动调用initFormData) |
|
|
loadFullContractData() |
|
|
loadFullContractData() |
|
|
|
|
|
|
|
|
// 初始化表单数据 |
|
|
// 初始化表单数据 |
|
|
@ -666,15 +925,6 @@ watch(() => props.show, (visible) => { |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
// 监听合同数据变化 |
|
|
|
|
|
watch(() => props.contractData, (newData) => { |
|
|
|
|
|
if (newData && props.show) { |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
initFormData() |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
}, { deep: true }) |
|
|
|
|
|
</script> |
|
|
</script> |
|
|
|
|
|
|
|
|
<style lang="scss" scoped> |
|
|
<style lang="scss" scoped> |
|
|
@ -700,13 +950,21 @@ watch(() => props.contractData, (newData) => { |
|
|
|
|
|
|
|
|
:deep(.el-dialog__body) { |
|
|
:deep(.el-dialog__body) { |
|
|
padding: 0; |
|
|
padding: 0; |
|
|
max-height: 85vh; |
|
|
max-height: 75vh; |
|
|
overflow-y: auto; |
|
|
overflow: hidden; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.sign-content { |
|
|
.sign-content { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
height: 100%; |
|
|
|
|
|
overflow: hidden; |
|
|
|
|
|
|
|
|
.contract-header { |
|
|
.contract-header { |
|
|
|
|
|
flex-shrink: 0; |
|
|
padding: 24px 32px; |
|
|
padding: 24px 32px; |
|
|
background: #fafbfc; |
|
|
background: #fafbfc; |
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
@ -741,202 +999,226 @@ watch(() => props.contractData, (newData) => { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.contract-main { |
|
|
.contract-content-wrapper { |
|
|
display: flex; |
|
|
|
|
|
gap: 24px; |
|
|
|
|
|
padding: 24px 32px; |
|
|
|
|
|
min-height: 500px; |
|
|
|
|
|
|
|
|
|
|
|
.contract-content-panel { |
|
|
|
|
|
flex: 1; |
|
|
flex: 1; |
|
|
min-width: 0; |
|
|
|
|
|
border: 1px solid #e5e7eb; |
|
|
|
|
|
border-radius: 12px; |
|
|
|
|
|
background: white; |
|
|
|
|
|
display: flex; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 24px; |
|
|
|
|
|
min-height: 0; |
|
|
|
|
|
overflow: hidden; |
|
|
|
|
|
|
|
|
.panel-header { |
|
|
.contract-content-panel { |
|
|
padding: 20px; |
|
|
flex: 1; |
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
min-width: 0; |
|
|
|
|
|
border: 1px solid #e5e7eb; |
|
|
|
|
|
border-radius: 12px; |
|
|
|
|
|
background: white; |
|
|
display: flex; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
|
|
|
background: #f8fafc; |
|
|
|
|
|
border-radius: 12px 12px 0 0; |
|
|
|
|
|
|
|
|
|
|
|
h3 { |
|
|
|
|
|
margin: 0; |
|
|
|
|
|
font-size: 16px; |
|
|
|
|
|
font-weight: 600; |
|
|
|
|
|
color: #1f2937; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.panel-actions { |
|
|
.panel-header { |
|
|
|
|
|
padding: 20px; |
|
|
|
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
display: flex; |
|
|
display: flex; |
|
|
gap: 8px; |
|
|
justify-content: space-between; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
background: #f8fafc; |
|
|
|
|
|
border-radius: 12px 12px 0 0; |
|
|
|
|
|
|
|
|
|
|
|
h3 { |
|
|
|
|
|
margin: 0; |
|
|
|
|
|
font-size: 16px; |
|
|
|
|
|
font-weight: 600; |
|
|
|
|
|
color: #1f2937; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.panel-actions { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
gap: 8px; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.contract-content-display { |
|
|
.contract-content-display { |
|
|
flex: 1; |
|
|
flex: 1; |
|
|
padding: 24px; |
|
|
padding: 24px; |
|
|
overflow-y: auto; |
|
|
overflow-y: auto; |
|
|
font-size: 14px; |
|
|
font-size: 14px; |
|
|
line-height: 1.8; |
|
|
line-height: 1.8; |
|
|
color: #374151; |
|
|
color: #374151; |
|
|
|
|
|
|
|
|
.content-text { |
|
|
.content-text { |
|
|
:deep(.filled-placeholder) { |
|
|
:deep(.filled-placeholder) { |
|
|
background: #dcfce7; |
|
|
background: #dcfce7; |
|
|
color: #166534; |
|
|
color: #166534; |
|
|
padding: 2px 6px; |
|
|
padding: 2px 6px; |
|
|
border-radius: 4px; |
|
|
border-radius: 4px; |
|
|
font-weight: 500; |
|
|
font-weight: 500; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
:deep(.empty-placeholder) { |
|
|
|
|
|
background: #fef3c7; |
|
|
|
|
|
color: #92400e; |
|
|
|
|
|
padding: 2px 6px; |
|
|
|
|
|
border-radius: 4px; |
|
|
|
|
|
font-style: italic; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
:deep(.empty-placeholder) { |
|
|
.empty-content { |
|
|
background: #fef3c7; |
|
|
text-align: center; |
|
|
color: #92400e; |
|
|
color: #9ca3af; |
|
|
padding: 2px 6px; |
|
|
|
|
|
border-radius: 4px; |
|
|
|
|
|
font-style: italic; |
|
|
font-style: italic; |
|
|
|
|
|
padding: 60px 20px; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.empty-content { |
|
|
|
|
|
text-align: center; |
|
|
|
|
|
color: #9ca3af; |
|
|
|
|
|
font-style: italic; |
|
|
|
|
|
padding: 60px 20px; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.contract-fields-panel { |
|
|
|
|
|
flex: 0 0 420px; |
|
|
|
|
|
border: 1px solid #e5e7eb; |
|
|
|
|
|
border-radius: 12px; |
|
|
|
|
|
background: white; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
|
|
|
|
|
|
.panel-header { |
|
|
.contract-fields-panel { |
|
|
padding: 20px; |
|
|
flex: 0 0 420px; |
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
border: 1px solid #e5e7eb; |
|
|
|
|
|
border-radius: 12px; |
|
|
|
|
|
background: white; |
|
|
display: flex; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
|
|
|
background: #f8fafc; |
|
|
|
|
|
border-radius: 12px 12px 0 0; |
|
|
|
|
|
|
|
|
|
|
|
h3 { |
|
|
|
|
|
margin: 0; |
|
|
|
|
|
font-size: 16px; |
|
|
|
|
|
font-weight: 600; |
|
|
|
|
|
color: #1f2937; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.field-stats { |
|
|
.panel-header { |
|
|
|
|
|
padding: 20px; |
|
|
|
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
display: flex; |
|
|
display: flex; |
|
|
gap: 8px; |
|
|
justify-content: space-between; |
|
|
} |
|
|
align-items: center; |
|
|
} |
|
|
background: #f8fafc; |
|
|
|
|
|
border-radius: 12px 12px 0 0; |
|
|
.fields-form { |
|
|
|
|
|
flex: 1; |
|
|
h3 { |
|
|
padding: 20px; |
|
|
margin: 0; |
|
|
overflow-y: auto; |
|
|
font-size: 16px; |
|
|
|
|
|
font-weight: 600; |
|
|
.fields-container { |
|
|
color: #1f2937; |
|
|
.field-item { |
|
|
} |
|
|
margin-bottom: 20px; |
|
|
|
|
|
padding: 16px; |
|
|
|
|
|
border: 1px solid #f0f0f0; |
|
|
|
|
|
border-radius: 8px; |
|
|
|
|
|
transition: all 0.3s ease; |
|
|
|
|
|
|
|
|
|
|
|
&.required { |
|
|
|
|
|
border-left: 4px solid #ef4444; |
|
|
|
|
|
background: #fef2f2; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
&.editable { |
|
|
|
|
|
border-left: 4px solid #3b82f6; |
|
|
|
|
|
background: #eff6ff; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
&.readonly { |
|
|
.field-stats { |
|
|
border-left: 4px solid #e5e7eb; |
|
|
display: flex; |
|
|
background: #f9fafb; |
|
|
gap: 8px; |
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
&:hover { |
|
|
.fields-form { |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
|
|
flex: 1; |
|
|
transform: translateY(-2px); |
|
|
padding: 20px; |
|
|
} |
|
|
overflow-y: auto; |
|
|
|
|
|
|
|
|
|
|
|
.fields-container { |
|
|
|
|
|
.field-item { |
|
|
|
|
|
margin-bottom: 20px; |
|
|
|
|
|
padding: 16px; |
|
|
|
|
|
border: 1px solid #f0f0f0; |
|
|
|
|
|
border-radius: 8px; |
|
|
|
|
|
transition: all 0.3s ease; |
|
|
|
|
|
|
|
|
|
|
|
&.required { |
|
|
|
|
|
border-left: 4px solid #ef4444; |
|
|
|
|
|
background: #fef2f2; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
:deep(.el-form-item__label) { |
|
|
&.editable { |
|
|
font-weight: 600; |
|
|
border-left: 4px solid #3b82f6; |
|
|
color: #374151; |
|
|
background: #eff6ff; |
|
|
font-size: 14px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.field-input { |
|
|
&.readonly { |
|
|
.field-icon { |
|
|
border-left: 4px solid #e5e7eb; |
|
|
color: #10b981; |
|
|
background: #f9fafb; |
|
|
font-size: 16px; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.readonly-field { |
|
|
&:hover { |
|
|
display: flex; |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
|
|
align-items: center; |
|
|
transform: translateY(-2px); |
|
|
gap: 8px; |
|
|
} |
|
|
padding: 8px 12px; |
|
|
|
|
|
background: #f3f4f6; |
|
|
|
|
|
border-radius: 6px; |
|
|
|
|
|
|
|
|
|
|
|
.field-value { |
|
|
:deep(.el-form-item__label) { |
|
|
flex: 1; |
|
|
font-weight: 600; |
|
|
color: #374151; |
|
|
color: #374151; |
|
|
font-weight: 500; |
|
|
font-size: 14px; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.signature-field { |
|
|
.field-input { |
|
|
.signature-preview { |
|
|
.field-icon { |
|
|
|
|
|
color: #10b981; |
|
|
|
|
|
font-size: 16px; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.readonly-field { |
|
|
display: flex; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
gap: 8px; |
|
|
|
|
|
padding: 8px 12px; |
|
|
|
|
|
background: #f3f4f6; |
|
|
|
|
|
border-radius: 6px; |
|
|
|
|
|
|
|
|
|
|
|
.field-value { |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
color: #374151; |
|
|
|
|
|
font-weight: 500; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.no-signature { |
|
|
.signature-field { |
|
|
color: #9ca3af; |
|
|
.signature-preview { |
|
|
font-style: italic; |
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
gap: 8px; |
|
|
|
|
|
padding: 8px 12px; |
|
|
|
|
|
background: #f0f9ff; |
|
|
|
|
|
border: 1px solid #bfdbfe; |
|
|
|
|
|
border-radius: 6px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.signature-action { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
justify-content: center; |
|
|
|
|
|
padding: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.signature-upload { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
justify-content: center; |
|
|
|
|
|
padding: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.no-signature { |
|
|
|
|
|
color: #9ca3af; |
|
|
|
|
|
font-style: italic; |
|
|
|
|
|
padding: 8px 12px; |
|
|
|
|
|
text-align: center; |
|
|
|
|
|
background: #f9fafb; |
|
|
|
|
|
border: 1px dashed #d1d5db; |
|
|
|
|
|
border-radius: 6px; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.field-info { |
|
|
.field-info { |
|
|
margin-top: 8px; |
|
|
margin-top: 8px; |
|
|
display: flex; |
|
|
display: flex; |
|
|
gap: 4px; |
|
|
gap: 4px; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.empty-fields { |
|
|
.empty-fields { |
|
|
flex: 1; |
|
|
flex: 1; |
|
|
display: flex; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
justify-content: center; |
|
|
padding: 60px 20px; |
|
|
padding: 60px 20px; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.signature-section { |
|
|
.signature-section { |
|
|
|
|
|
flex-shrink: 0; |
|
|
padding: 24px 32px; |
|
|
padding: 24px 32px; |
|
|
background: #f8fafc; |
|
|
background: #f8fafc; |
|
|
border-top: 1px solid #e5e7eb; |
|
|
border-top: 1px solid #e5e7eb; |
|
|
@ -1026,7 +1308,6 @@ watch(() => props.contractData, (newData) => { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.dialog-footer { |
|
|
.dialog-footer { |
|
|
padding: 20px 32px; |
|
|
padding: 20px 32px; |
|
|
@ -1049,7 +1330,26 @@ watch(() => props.contractData, (newData) => { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 美化滚动条 |
|
|
// 美化滚动条 |
|
|
:deep(.el-dialog__body), |
|
|
:deep(.el-dialog__body) { |
|
|
|
|
|
&::-webkit-scrollbar { |
|
|
|
|
|
width: 6px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-track { |
|
|
|
|
|
background: #f1f1f1; |
|
|
|
|
|
border-radius: 3px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-thumb { |
|
|
|
|
|
background: #c1c1c1; |
|
|
|
|
|
border-radius: 3px; |
|
|
|
|
|
|
|
|
|
|
|
&:hover { |
|
|
|
|
|
background: #a8a8a8; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
.contract-content-display, |
|
|
.contract-content-display, |
|
|
.fields-form { |
|
|
.fields-form { |
|
|
&::-webkit-scrollbar { |
|
|
&::-webkit-scrollbar { |
|
|
|