Browse Source

修改 bug

develop
王泽彦 4 months ago
parent
commit
b354e07865
  1. 253
      admin/src/app/views/campus/campus.vue
  2. 12
      admin/src/app/views/contract/contract.vue
  3. 672
      admin/src/app/views/personnel/components/ContractSignDialog.vue
  4. 22
      admin/src/utils/common.ts
  5. 98
      admin/src/utils/directUpload.ts
  6. 4
      niucloud/app/adminapi/controller/contract_sign/ContractSign.php
  7. 52
      niucloud/app/api/controller/apiController/Contract.php
  8. 47
      niucloud/app/service/admin/contract/ContractDistributionService.php
  9. 76
      niucloud/app/service/admin/contract_sign/ContractSignService.php
  10. 78
      niucloud/app/service/admin/document/DocumentTemplateService.php
  11. 5
      niucloud/app/service/admin/personnel/PersonnelService.php
  12. 9
      niucloud/app/service/api/apiService/ContractService.php
  13. 101
      niucloud/app/service/api/apiService/ContractSignFormService.php
  14. 5
      uniapp/api/apiRoute.js
  15. 52
      uniapp/pages-common/contract/my_contract.vue
  16. 73
      uniapp/pages-common/contract/staff-contract-sign.vue

253
admin/src/app/views/campus/campus.vue

@ -147,26 +147,60 @@
<el-input v-model="sealDialog.form.campus_name" disabled /> <el-input v-model="sealDialog.form.campus_name" disabled />
</el-form-item> </el-form-item>
<el-form-item label="签章图片" required> <el-form-item label="签章图片" required>
<el-upload <div class="seal-upload-container">
ref="uploadRef" <!-- 无图片时显示上传组件 -->
class="upload-demo" <el-upload
:action="uploadAction" v-if="sealDialog.fileList.length === 0"
:headers="uploadHeaders" ref="uploadRef"
:before-upload="beforeUpload" class="seal-upload"
:on-success="onUploadSuccess" :action="uploadAction"
:on-error="onUploadError" :headers="uploadHeaders"
:file-list="sealDialog.fileList" :before-upload="beforeUpload"
list-type="picture-card" :on-success="onUploadSuccess"
:limit="1" :on-error="onUploadError"
accept=".jpg,.jpeg,.png,.gif" :file-list="sealDialog.fileList"
> list-type="picture-card"
<el-icon><Plus /></el-icon> :limit="1"
<template #tip> accept=".jpg,.jpeg,.png,.gif"
<div class="el-upload__tip"> :show-file-list="false"
只能上传jpg/png/gif文件且不超过2MB >
<div class="upload-placeholder">
<el-icon class="upload-icon"><Plus /></el-icon>
<div class="upload-text">上传签章</div>
</div>
<template #tip>
<div class="upload-tip">
支持 jpg/png/gif 格式文件大小不超过 2MB
</div>
</template>
</el-upload>
<!-- 有图片时显示图片预览和操作按钮 -->
<div v-else class="seal-preview-container">
<div class="seal-image-wrapper">
<img
:src="sealDialog.fileList[0].url"
alt="签章图片"
class="seal-image"
/>
<div class="seal-image-actions">
<el-button
type="danger"
size="small"
@click="onRemoveFile"
class="remove-btn"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<div class="seal-info">
<div class="seal-name">{{ sealDialog.fileList[0].name }}</div>
<div class="seal-status text-success">已上传</div>
</div> </div>
</template> </div>
</el-upload> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@ -194,7 +228,7 @@ import { useDictionary } from '@/app/api/dict'
import { getCampusList, deleteCampus, uploadCampusSeal } from '@/app/api/campus' import { getCampusList, deleteCampus, uploadCampusSeal } from '@/app/api/campus'
import { img } from '@/utils/common' import { img } from '@/utils/common'
import { ElMessageBox, ElMessage, FormInstance } from 'element-plus' import { ElMessageBox, ElMessage, FormInstance } from 'element-plus'
import { Plus } from '@element-plus/icons-vue' import { Plus, Delete } from '@element-plus/icons-vue'
import Edit from '@/app/views/campus/components/campus-edit.vue' import Edit from '@/app/views/campus/components/campus-edit.vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { getToken } from '@/utils/common' import { getToken } from '@/utils/common'
@ -318,8 +352,19 @@ const resetForm = (formEl: FormInstance | undefined) => {
const uploadSealEvent = (data: any) => { const uploadSealEvent = (data: any) => {
sealDialog.form.campus_id = data.id sealDialog.form.campus_id = data.id
sealDialog.form.campus_name = data.campus_name sealDialog.form.campus_name = data.campus_name
sealDialog.form.seal_image = '' sealDialog.form.seal_image = data.seal_image || ''
sealDialog.fileList = []
//
if (data.seal_image) {
sealDialog.fileList = [{
name: '签章图片',
url: data.seal_image,
uid: Date.now()
}]
} else {
sealDialog.fileList = []
}
sealDialog.visible = true sealDialog.visible = true
} }
@ -347,6 +392,14 @@ const beforeUpload = (file: any) => {
const onUploadSuccess = (response: any, file: any) => { const onUploadSuccess = (response: any, file: any) => {
if (response.code === 1) { if (response.code === 1) {
sealDialog.form.seal_image = response.data.url sealDialog.form.seal_image = response.data.url
//
sealDialog.fileList = [{
name: file.name || '签章图片',
url: response.data.url,
uid: file.uid || Date.now()
}]
ElMessage.success('图片上传成功') ElMessage.success('图片上传成功')
} else { } else {
ElMessage.error(response.msg || '上传失败') ElMessage.error(response.msg || '上传失败')
@ -362,26 +415,31 @@ const onUploadError = (error: any) => {
sealDialog.fileList = [] sealDialog.fileList = []
} }
/**
* 移除文件回调
*/
const onRemoveFile = () => {
sealDialog.form.seal_image = ''
sealDialog.fileList = []
}
/** /**
* 保存签章 * 保存签章
*/ */
const saveSeal = async () => { const saveSeal = async () => {
if (!sealDialog.form.seal_image) {
ElMessage.warning('请先上传签章图片')
return
}
sealDialog.loading = true sealDialog.loading = true
try { try {
await uploadCampusSeal({ await uploadCampusSeal({
campus_id: sealDialog.form.campus_id, campus_id: sealDialog.form.campus_id,
seal_image: sealDialog.form.seal_image seal_image: sealDialog.form.seal_image || null
}) })
sealDialog.visible = false sealDialog.visible = false
loadCampusList() // loadCampusList() //
ElMessage.success('签章保存成功')
} catch (error) { } catch (error) {
console.error('签章上传失败:', error) console.error('签章保存失败:', error)
ElMessage.error('签章保存失败')
} finally { } finally {
sealDialog.loading = false sealDialog.loading = false
} }
@ -399,47 +457,116 @@ const saveSeal = async () => {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
/* 修复上传组件在弹窗中的样式 */ /* 签章上传容器样式 */
.upload-demo { .seal-upload-container {
width: 100%; width: 100%;
}
:deep(.el-upload-list--picture-card) {
--el-upload-list-picture-card-size: 100px; /* 上传组件样式 */
display: flex; .seal-upload {
flex-wrap: wrap; width: 100%;
margin: 0 0 10px 0;
}
:deep(.el-upload--picture-card) { :deep(.el-upload--picture-card) {
--el-upload-picture-card-size: 100px; width: 200px;
width: var(--el-upload-picture-card-size); height: 120px;
height: var(--el-upload-picture-card-size); border: 2px dashed var(--el-border-color-lighter);
line-height: calc(var(--el-upload-picture-card-size) - 2px); border-radius: 8px;
margin-bottom: 10px; background-color: var(--el-fill-color-blank);
} transition: all 0.3s;
:deep(.el-upload-list__item) { &:hover {
width: var(--el-upload-list-picture-card-size); border-color: var(--el-color-primary);
height: var(--el-upload-list-picture-card-size); background-color: var(--el-color-primary-light-9);
margin: 0 8px 10px 0; }
} }
/* 确保上传区域不超出弹窗 */ .upload-placeholder {
:deep(.el-upload-dragger) { display: flex;
width: 100%; flex-direction: column;
height: 100px; align-items: center;
justify-content: center;
height: 100%;
color: var(--el-text-color-regular);
.upload-icon {
font-size: 24px;
margin-bottom: 8px;
color: var(--el-color-primary);
}
.upload-text {
font-size: 14px;
font-weight: 500;
}
} }
/* 修复提示文字被盖住的问题 */
:deep(.el-upload__tip) { :deep(.el-upload__tip) {
margin-top: 25%; margin-top: 12px;
line-height: 1.4; text-align: center;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
font-size: 12px; font-size: 12px;
line-height: 1.4;
}
}
/* 图片预览容器样式 */
.seal-preview-container {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
background-color: var(--el-fill-color-blank);
.seal-image-wrapper {
position: relative; position: relative;
z-index: 1; display: inline-block;
clear: both;
display: block; .seal-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 6px;
border: 1px solid var(--el-border-color-lighter);
}
.seal-image-actions {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.3s;
.remove-btn {
padding: 8px 12px;
font-size: 12px;
}
}
&:hover .seal-image-actions {
opacity: 1;
}
}
.seal-info {
flex: 1;
.seal-name {
font-size: 14px;
color: var(--el-text-color-primary);
margin-bottom: 4px;
font-weight: 500;
}
.seal-status {
font-size: 12px;
&.text-success {
color: var(--el-color-success);
}
}
} }
} }
</style> </style>

12
admin/src/app/views/contract/contract.vue

@ -173,10 +173,7 @@
<div class="dialog-body"> <div class="dialog-body">
<div class="config-section"> <div class="config-section">
<h4>配置说明</h4> <h4>配置说明</h4>
<ul> <ul v-html="placeholderHelpText">
<li>占位符格式双大括号包围例如{\t{学员姓名}\t}</li>
<li>请为每个占位符配置对应的数据源表和字段</li>
<li>必填项在生成合同时必须有值否则会报错</li>
</ul> </ul>
</div> </div>
@ -1695,6 +1692,13 @@ const submitUpload = async () => {
} }
} }
//
const placeholderHelpText = `
<li>占位符格式双大括号包围例如&#123;&#123;学员姓名&#125;&#125;</li>
<li>请为每个占位符配置对应的数据源表和字段</li>
<li>必填项在生成合同时必须有值否则会报错</li>
`
onMounted(() => { onMounted(() => {
getList() getList()
}) })

672
admin/src/app/views/personnel/components/ContractSignDialog.vue

@ -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_configsvalue
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 {
@ -1070,4 +1370,4 @@ watch(() => props.contractData, (newData) => {
} }
} }
} }
</style> </style>

22
admin/src/utils/common.ts

@ -143,6 +143,28 @@ export function assetImg(path: string) {
return new URL('@/', import.meta.url) + path return new URL('@/', import.meta.url) + path
} }
/**
* 便
* @param file -
* @param options -
* @returns Promise<UploadResult>
*/
export async function uploadImage(file: File, options?: {
onProgress?: (percent: number) => void
onSuccess?: (result: { success: boolean; url: string }) => void
onError?: (error: string) => void
}) {
const { directUpload } = await import('./directUpload')
return directUpload.upload({
file,
fileType: 'image',
onProgress: options?.onProgress,
onSuccess: options?.onSuccess,
onError: options?.onError
})
}
/** /**
* *
* @param str * @param str

98
admin/src/utils/directUpload.ts

@ -336,6 +336,104 @@ export async function uploadFile(options: UploadOptions): Promise<UploadResult>
return directUpload.upload(options) return directUpload.upload(options)
} }
/**
* 便
* @param file -
* @param options -
*/
export async function uploadImage(file: File, options?: {
onProgress?: (percent: number) => void
onSuccess?: (url: string) => void
onError?: (error: string) => void
}): Promise<UploadResult> {
return directUpload.upload({
file,
fileType: 'image',
onProgress: options?.onProgress,
onSuccess: options?.onSuccess ? (result) => options.onSuccess!(result.url) : undefined,
onError: options?.onError
})
}
/**
* 便
* @param file -
* @param options -
*/
export async function uploadVideo(file: File, options?: {
onProgress?: (percent: number) => void
onSuccess?: (url: string) => void
onError?: (error: string) => void
}): Promise<UploadResult> {
return directUpload.upload({
file,
fileType: 'video',
onProgress: options?.onProgress,
onSuccess: options?.onSuccess ? (result) => options.onSuccess!(result.url) : undefined,
onError: options?.onError
})
}
/**
* 便
* @param file -
* @param options -
*/
export async function uploadDocument(file: File, options?: {
onProgress?: (percent: number) => void
onSuccess?: (url: string) => void
onError?: (error: string) => void
}): Promise<UploadResult> {
return directUpload.upload({
file,
fileType: 'document',
onProgress: options?.onProgress,
onSuccess: options?.onSuccess ? (result) => options.onSuccess!(result.url) : undefined,
onError: options?.onError
})
}
/**
*
* @param files -
* @param options -
*/
export async function uploadFiles(files: File[], options?: {
fileType: 'image' | 'video' | 'document'
onProgress?: (fileIndex: number, percent: number) => void
onSuccess?: (fileIndex: number, url: string) => void
onError?: (fileIndex: number, error: string) => void
onAllComplete?: (results: UploadResult[]) => void
}): Promise<UploadResult[]> {
const results: UploadResult[] = []
for (let i = 0; i < files.length; i++) {
try {
const result = await directUpload.upload({
file: files[i],
fileType: options?.fileType || 'image',
onProgress: (percent) => {
options?.onProgress?.(i, percent)
},
onSuccess: (uploadResult) => {
options?.onSuccess?.(i, uploadResult.url)
},
onError: (error) => {
options?.onError?.(i, error)
}
})
results.push(result)
} catch (error) {
const errorMsg = error instanceof Error ? error.message : '上传失败'
options?.onError?.(i, errorMsg)
results.push({ success: false, url: '', error: errorMsg })
}
}
options?.onAllComplete?.(results)
return results
}
/** /**
* *
* *

4
niucloud/app/adminapi/controller/contract_sign/ContractSign.php

@ -81,6 +81,10 @@ class ContractSign extends BaseAdminController
$data = $this->request->params([ $data = $this->request->params([
["contract_id",0], ["contract_id",0],
["personnel_id",0], ["personnel_id",0],
["fill_data",""],
["sign_time",""],
["signature_image",""],
["status",1]
]); ]);
$this->validate($data, 'app\validate\contract_sign\ContractSign.edit'); $this->validate($data, 'app\validate\contract_sign\ContractSign.edit');

52
niucloud/app/api/controller/apiController/Contract.php

@ -163,30 +163,72 @@ class Contract extends BaseApiService
public function getSignForm(Request $request) public function getSignForm(Request $request)
{ {
$contract_id = $request->param('contract_id', 0); $contract_id = $request->param('contract_id', 0);
if (empty($contract_id)) { if (empty($contract_id)) {
return fail('合同ID不能为空'); return fail('合同ID不能为空');
} }
$where = [ $where = [
'contract_id' => $contract_id, 'contract_id' => $contract_id,
'personnel_id' => $this->member_id 'personnel_id' => $this->member_id
]; ];
try { try {
$service = new ContractService(); $service = new ContractService();
$res = $service->getStaffContractSignForm($where); $res = $service->getStaffContractSignForm($where);
if (!$res['code']) { if (!$res['code']) {
return fail($res['msg']); return fail($res['msg']);
} }
return success($res['data']); return success($res['data']);
} catch (\Exception $e) { } catch (\Exception $e) {
return fail('获取签署表单失败:' . $e->getMessage()); return fail('获取签署表单失败:' . $e->getMessage());
} }
} }
/**
* 获取合同签署表单(根据contract_id获取完整数据)
* @param Request $request
* @return mixed
*/
public function signForm(Request $request)
{
$contract_id = $request->param('contract_id', 0);
Log::info('signForm方法被调用', [
'contract_id' => $contract_id,
'member_id' => $this->member_id
]);
if (empty($contract_id)) {
return fail('合同ID不能为空');
}
try {
$service = new ContractSignFormService();
$res = $service->getContractSignFormByContractId($contract_id);
Log::info('signForm服务调用结果', [
'contract_id' => $contract_id,
'result_code' => $res['code'],
'result_msg' => $res['msg'] ?? ''
]);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data']);
} catch (\Exception $e) {
Log::error('signForm异常', [
'contract_id' => $contract_id,
'error' => $e->getMessage()
]);
return fail('获取签署表单失败:' . $e->getMessage());
}
}
/** /**
* 下载合同文件 * 下载合同文件
* @param Request $request * @param Request $request

47
niucloud/app/service/admin/contract/ContractDistributionService.php

@ -301,13 +301,13 @@ class ContractDistributionService extends BaseAdminService
private function copyDataSourceConfig(int $contractId, int $contractSignId): bool private function copyDataSourceConfig(int $contractId, int $contractSignId): bool
{ {
try { try {
// 获取原合同的模板配置(contract_sign_id 为 null 的记录) // 从 school_contract 表获取占位符配置
$sourceConfigs = Db::table('school_document_data_source_config') $contract = Db::table('school_contract')
->where('contract_id', $contractId) ->where('id', $contractId)
->where('contract_sign_id', null) ->field('placeholder_config')
->select(); ->find();
if (empty($sourceConfigs)) { if (empty($contract) || empty($contract['placeholder_config'])) {
// 如果没有模板配置,记录日志但不阻止分发流程 // 如果没有模板配置,记录日志但不阻止分发流程
Log::info('合同无数据源配置模板', [ Log::info('合同无数据源配置模板', [
'contract_id' => $contractId, 'contract_id' => $contractId,
@ -316,23 +316,34 @@ class ContractDistributionService extends BaseAdminService
return true; return true;
} }
// 解析占位符配置
$placeholderConfig = json_decode($contract['placeholder_config'], true);
if (empty($placeholderConfig)) {
Log::info('合同占位符配置为空', [
'contract_id' => $contractId,
'contract_sign_id' => $contractSignId
]);
return true;
}
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
$insertData = []; $insertData = [];
foreach ($sourceConfigs as $config) { // 为每个占位符创建配置记录
foreach ($placeholderConfig as $placeholder => $config) {
$insertData[] = [ $insertData[] = [
'contract_id' => $config['contract_id'], 'contract_id' => $contractId,
'contract_sign_id' => $contractSignId, 'contract_sign_id' => $contractSignId,
'placeholder' => $config['placeholder'], 'placeholder' => $placeholder,
'data_type' => $config['data_type'], 'data_type' => $config['data_type'] ?? 'user_input',
'table_name' => $config['table_name'], 'table_name' => $config['table_name'] ?? '',
'field_name' => $config['field_name'], 'field_name' => $config['field_name'] ?? '',
'field_type' => $config['field_type'], 'field_type' => $config['field_type'] ?? 'text',
'is_required' => $config['is_required'], 'is_required' => $config['is_required'] ?? 0,
'default_value' => $config['default_value'], 'default_value' => $config['default_value'] ?? '',
'system_function' => $config['system_function'], 'system_function' => $config['system_function'] ?? '',
'sign_party' => $config['sign_party'], 'sign_party' => $config['sign_party'] ?? '',
'validation_rule' => $config['validation_rule'], 'validation_rule' => $config['validation_rule'] ?? '',
'created_at' => $now, 'created_at' => $now,
'updated_at' => $now 'updated_at' => $now
]; ];

76
niucloud/app/service/admin/contract_sign/ContractSignService.php

@ -145,6 +145,11 @@ class ContractSignService extends BaseAdminService
*/ */
public function edit(int $id, array $data) public function edit(int $id, array $data)
{ {
// 处理fill_data字段的特殊逻辑
if (isset($data['fill_data']) && !empty($data['fill_data'])) {
// 解析fill_data并更新school_document_data_source_config表
$this->updateDocumentDataSourceConfig($id, $data['fill_data']);
}
$this->model->where([['id', '=', $id]])->update($data); $this->model->where([['id', '=', $id]])->update($data);
return true; return true;
@ -440,5 +445,76 @@ class ContractSignService extends BaseAdminService
} }
} }
/**
* 更新文档数据源配置的default_value字段
* @param int $contractSignId
* @param string $fillData
* @return bool
*/
private function updateDocumentDataSourceConfig(int $contractSignId, string $fillData): bool
{
try {
// 解析fill_data JSON
$fillDataArray = json_decode($fillData, true);
if (empty($fillDataArray) || !is_array($fillDataArray)) {
return false;
}
// 获取当前合同签署记录的contract_id
$contractSign = $this->model->where('id', $contractSignId)->find();
if (empty($contractSign)) {
return false;
}
$contractId = $contractSign->contract_id;
// 获取所有相关的数据源配置
$configs = \think\facade\Db::table('school_document_data_source_config')
->where('contract_id', $contractId)
->where('contract_sign_id', $contractSignId)
->select();
if (empty($configs)) {
// 如果没有特定配置,查找通用配置并复制一份
$generalConfigs = \think\facade\Db::table('school_document_data_source_config')
->where('contract_id', $contractId)
->where('contract_sign_id', null)
->select();
if (!empty($generalConfigs)) {
foreach ($generalConfigs as $config) {
$newConfig = $config->toArray();
unset($newConfig['id']); // 移除ID以创建新记录
$newConfig['contract_sign_id'] = $contractSignId;
// 设置default_value
$placeholder = $newConfig['placeholder'];
if (isset($fillDataArray[$placeholder])) {
$newConfig['default_value'] = $fillDataArray[$placeholder];
}
// 插入新配置
\think\facade\Db::table('school_document_data_source_config')->insert($newConfig);
}
}
} else {
// 更新现有配置
foreach ($configs as $config) {
$placeholder = $config['placeholder'];
if (isset($fillDataArray[$placeholder])) {
\think\facade\Db::table('school_document_data_source_config')
->where('id', $config['id'])
->update(['default_value' => $fillDataArray[$placeholder]]);
}
}
}
return true;
} catch (\Exception $e) {
// 记录错误日志
\think\facade\Log::error('更新文档数据源配置失败: ' . $e->getMessage());
return false;
}
}
} }

78
niucloud/app/service/admin/document/DocumentTemplateService.php

@ -83,15 +83,16 @@ class DocumentTemplateService extends BaseAdminService
$info['placeholders'] = $info['placeholders'] ? json_decode($info['placeholders'], true) : []; $info['placeholders'] = $info['placeholders'] ? json_decode($info['placeholders'], true) : [];
$info['file_size_formatted'] = $this->formatFileSize($info['file_size']); $info['file_size_formatted'] = $this->formatFileSize($info['file_size']);
// 获取数据源配置信息,从school_document_data_source_config表获取 // 基于placeholder_config构建数据源配置,不查询school_document_data_source_config表
$dataSourceConfigs = []; $dataSourceConfigs = [];
$dbConfigs = $this->dataSourceModel->where('contract_id', $id)->select()->toArray(); if (!empty($info['placeholders'])) {
foreach ($info['placeholders'] as $placeholder) {
// 使用placeholder_config中的配置,如果没有则使用默认配置
$config = $info['placeholder_config'][$placeholder] ?? [];
if (!empty($dbConfigs)) {
foreach ($dbConfigs as $config) {
$dataSourceConfigs[] = [ $dataSourceConfigs[] = [
'id' => $config['id'], 'id' => 0,
'placeholder' => $config['placeholder'], 'placeholder' => $placeholder,
'data_type' => $config['data_type'] ?? 'user_input', 'data_type' => $config['data_type'] ?? 'user_input',
'table_name' => $config['table_name'] ?? '', 'table_name' => $config['table_name'] ?? '',
'field_name' => $config['field_name'] ?? '', 'field_name' => $config['field_name'] ?? '',
@ -105,38 +106,6 @@ class DocumentTemplateService extends BaseAdminService
} }
} }
// 为所有没有配置的占位符创建默认配置
if (!empty($info['placeholders'])) {
// 获取已配置的占位符列表
$configuredPlaceholders = array_column($dataSourceConfigs, 'placeholder');
// 为未配置的占位符创建默认配置
foreach ($info['placeholders'] as $placeholder) {
if (!in_array($placeholder, $configuredPlaceholders)) {
$dataSourceConfigs[] = [
'id' => 0,
'placeholder' => $placeholder,
'data_type' => 'user_input',
'table_name' => '',
'field_name' => '',
'system_function' => '',
'user_input_value' => '',
'sign_party' => '',
'field_type' => 'text',
'is_required' => 0,
'default_value' => ''
];
}
}
// 按占位符在文档中出现的顺序排序
usort($dataSourceConfigs, function($a, $b) use ($info) {
$aIndex = array_search($a['placeholder'], $info['placeholders']);
$bIndex = array_search($b['placeholder'], $info['placeholders']);
return $aIndex - $bIndex;
});
}
$info['data_source_configs'] = $dataSourceConfigs; $info['data_source_configs'] = $dataSourceConfigs;
} }
@ -448,12 +417,17 @@ class DocumentTemplateService extends BaseAdminService
try { try {
// 1. 保存配置到合同表的placeholder_config字段(保持兼容性) // 1. 保存配置到合同表的placeholder_config字段(保持兼容性)
$template->placeholder_config = json_encode($configData); $template->placeholder_config = json_encode($configData);
// 2. 更新placeholder字段为占位符的逗号分隔文本
$placeholders = array_keys($configData);
$template->placeholder = implode(',', $placeholders);
// 3. 更新placeholders字段为JSON数组
$template->placeholders = json_encode($placeholders);
$template->updated_at = date('Y-m-d H:i:s'); $template->updated_at = date('Y-m-d H:i:s');
$template->save(); $template->save();
// 2. 同时保存到独立的数据源配置表(用户期望的表)
$this->saveConfigToDataSourceTable($templateId, $configData);
\think\facade\Db::commit(); \think\facade\Db::commit();
return true; return true;
} catch (\Exception $e) { } catch (\Exception $e) {
@ -1458,10 +1432,15 @@ class DocumentTemplateService extends BaseAdminService
// 更新占位符配置 // 更新占位符配置
if (!empty($newConfig)) { if (!empty($newConfig)) {
$template->placeholder_config = json_encode($newConfig); $template->placeholder_config = json_encode($newConfig);
$template->save();
// 同步更新数据源配置表 // 更新placeholder字段为占位符的逗号分隔文本
$this->saveConfigToDataSourceTable($id, $newConfig); $placeholders = array_keys($newConfig);
$template->placeholder = implode(',', $placeholders);
// 更新placeholders字段为JSON数组
$template->placeholders = json_encode($placeholders);
$template->save();
} }
return [ return [
@ -1584,10 +1563,15 @@ class DocumentTemplateService extends BaseAdminService
// 更新占位符配置 // 更新占位符配置
if (!empty($newConfig)) { if (!empty($newConfig)) {
$template->placeholder_config = json_encode($newConfig); $template->placeholder_config = json_encode($newConfig);
$template->save();
// 同步更新数据源配置表 // 更新placeholder字段为占位符的逗号分隔文本
$this->saveConfigToDataSourceTable($id, $newConfig); $placeholders = array_keys($newConfig);
$template->placeholder = implode(',', $placeholders);
// 更新placeholders字段为JSON数组
$template->placeholders = json_encode($placeholders);
$template->save();
} }
return [ return [

5
niucloud/app/service/admin/personnel/PersonnelService.php

@ -169,8 +169,11 @@ class PersonnelService extends BaseAdminService
$data['sys_user_id'] = $uid; $data['sys_user_id'] = $uid;
} }
} }
$this->model->where([['id', '=', $id]])->update($data); $data['birthday'] = $info['birthday'];
$data['native_place'] = $info['native_place'];
unset($info['birthday']); unset($info['birthday']);
unset($info['native_place']);
$this->model->where([['id', '=', $id]])->update($data);
(new PersonnelInfo())->where(['person_id' => $id])->update($info); (new PersonnelInfo())->where(['person_id' => $id])->update($info);
Db::commit(); Db::commit();

9
niucloud/app/service/api/apiService/ContractService.php

@ -346,13 +346,12 @@ class ContractService extends BaseApiService
// 查询合同签署记录 // 查询合同签署记录
$contractSign = ContractSign::alias('cs') $contractSign = ContractSign::alias('cs')
->join('school_contract c', 'cs.contract_id = c.id') ->join('school_contract c', 'cs.contract_id = c.id')
->where('cs.contract_id', $contract_id) ->where('cs.id', $contract_id)
->where('cs.personnel_id', $personnel_id)
->where('cs.type', 1) // 员工合同
->where('cs.deleted_at', 0) ->where('cs.deleted_at', 0)
->where('c.deleted_at', 0) ->where('c.deleted_at', 0)
->field([ ->field([
'cs.id as sign_id', 'cs.id as sign_id',
'cs.contract_id',
'cs.status', 'cs.status',
'cs.fill_data', 'cs.fill_data',
'c.contract_name', 'c.contract_name',
@ -371,7 +370,7 @@ class ContractService extends BaseApiService
// 获取表单字段配置 // 获取表单字段配置
$formFields = Db::table('school_document_data_source_config') $formFields = Db::table('school_document_data_source_config')
->where('contract_id', $contract_id) ->where('contract_sign_id', $contract_id)
->order('id', 'asc') ->order('id', 'asc')
->select() ->select()
->toArray(); ->toArray();
@ -397,7 +396,7 @@ class ContractService extends BaseApiService
'msg' => '获取成功', 'msg' => '获取成功',
'data' => [ 'data' => [
'sign_id' => $contractData['sign_id'], 'sign_id' => $contractData['sign_id'],
'contract_id' => $contract_id, 'contract_id' => $contractData['contract_id'],
'contract_name' => $contractData['contract_name'], 'contract_name' => $contractData['contract_name'],
'contract_type' => $contractData['contract_type'], 'contract_type' => $contractData['contract_type'],
'contract_content' => $contractData['contract_content'] ?: '', 'contract_content' => $contractData['contract_content'] ?: '',

101
niucloud/app/service/api/apiService/ContractSignFormService.php

@ -129,6 +129,107 @@ class ContractSignFormService extends BaseApiService
} }
} }
/**
* 根据合同签署记录ID获取签署表单配置(完整的表单数据)
* 获取合同基本信息 + 签署记录 + 占位符配置的完整数据
*
* @param int $contract_sign_id 合同签署记录ID
* @return array 返回格式化的表单配置数据
*/
public function getContractSignFormByContractId($contract_sign_id)
{
try {
Log::info('signForm接口被调用', [
'contract_sign_id' => $contract_sign_id,
'member_id' => $this->member_id
]);
if (empty($contract_sign_id)) {
throw new \Exception('合同签署记录ID不能为空');
}
Log::info('开始根据合同签署记录ID获取签署表单', ['contract_sign_id' => $contract_sign_id]);
// 1. 根据签署记录ID获取签署信息
$sign_record = Db::table('school_contract_sign')
->where('id', $contract_sign_id)
->where('deleted_at', 0)
->find();
if (!$sign_record) {
Log::error('合同签署记录不存在', [
'contract_sign_id' => $contract_sign_id,
'query_result' => $sign_record
]);
throw new \Exception('合同签署记录不存在');
}
$contract_id = $sign_record['contract_id'];
// 2. 获取合同模板基本信息
$contract = $this->getContractInfo($contract_id);
if (!$contract) {
throw new \Exception('合同模板不存在或已删除');
}
// 3. 获取占位符配置信息
$form_fields = [];
$student_info = null;
if (!empty($sign_record['student_id'])) {
$student_info = $this->getStudentInfo($sign_record['student_id']);
}
// 获取该签署记录的专属字段配置
$form_fields = $this->getSignSpecificFormFields($contract_sign_id, $student_info ?? []);
// 4. 组装返回数据
$result = [
'contract_id' => $contract_id,
'contract_sign_id' => $contract_sign_id,
'contract_name' => $contract['contract_name'],
'contract_type' => $contract['contract_type'],
'contract_content' => $contract['contract_content'] ?? '',
'sign_record' => $sign_record,
'form_fields' => $form_fields
];
if ($student_info) {
$result['student_info'] = [
'id' => $student_info['id'],
'name' => $student_info['name'],
'phone' => $student_info['contact_phone'] ?? '',
'user_id' => $student_info['user_id']
];
}
Log::info('根据合同签署记录ID获取表单成功', [
'contract_sign_id' => $contract_sign_id,
'contract_id' => $contract_id,
'form_fields_count' => count($form_fields)
]);
return [
'code' => 1,
'msg' => '获取成功',
'data' => $result
];
} catch (\Exception $e) {
Log::error('根据合同签署记录ID获取签署表单失败', [
'contract_sign_id' => $contract_sign_id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'code' => 0,
'msg' => $e->getMessage(),
'data' => []
];
}
}
/** /**
* 获取学员合同签署表单配置 * 获取学员合同签署表单配置
* 该方法为移动端提供合同签署表单的完整配置信息 * 该方法为移动端提供合同签署表单的完整配置信息

5
uniapp/api/apiRoute.js

@ -949,6 +949,11 @@ export default {
return await http.get('/contract/detail', { id: contractId }) return await http.get('/contract/detail', { id: contractId })
}, },
// 获取合同签署表单配置
async getSignForm(contractId) {
return await http.get('/contract/signForm', { contract_id: contractId })
},
// 获取合同表单字段(暂时返回空,需要后端实现) // 获取合同表单字段(暂时返回空,需要后端实现)
async getContractFormFields(contractId) { async getContractFormFields(contractId) {
return { code: 1, data: [] } return { code: 1, data: [] }

52
uniapp/pages-common/contract/my_contract.vue

@ -3,18 +3,18 @@
<!-- 筛选栏 --> <!-- 筛选栏 -->
<view class="filter-bar"> <view class="filter-bar">
<view class="filter-item" <view class="filter-item"
:class="filterStatus === '' ? 'active' : ''" :class="filterStatus === '' ? 'active' : ''"
@click="filterContract('')"> @click="filterContract('')">
全部 全部
</view> </view>
<view class="filter-item" <view class="filter-item"
:class="filterStatus === 'unsigned' ? 'active' : ''" :class="filterStatus === 'unsigned' ? 'active' : ''"
@click="filterContract('unsigned')"> @click="filterContract('unsigned')">
待签订 待签订
</view> </view>
<view class="filter-item" <view class="filter-item"
:class="filterStatus === 'signed' ? 'active' : ''" :class="filterStatus === 'signed' ? 'active' : ''"
@click="filterContract('signed')"> @click="filterContract('signed')">
已签订 已签订
</view> </view>
@ -32,11 +32,11 @@
</view> </view>
<view v-else> <view v-else>
<view v-for="contract in filteredContracts" <view v-for="contract in filteredContracts"
:key="contract.id" :key="contract.id"
class="contract-item" class="contract-item"
@click="goToDetail(contract)"> @click="goToDetail(contract)">
<!-- 合同卡片 --> <!-- 合同卡片 -->
<view class="contract-card"> <view class="contract-card">
<!-- 合同标题和状态 --> <!-- 合同标题和状态 -->
@ -74,7 +74,7 @@
<button v-if="needSignButton(contract)" class="sign-btn" @click.stop="goToSign(contract)"> <button v-if="needSignButton(contract)" class="sign-btn" @click.stop="goToSign(contract)">
立即签订 立即签订
</button> </button>
<!-- 已签署状态显示预览和下载按钮 --> <!-- 已签署状态显示预览和下载按钮 -->
<view v-else class="signed-actions"> <view v-else class="signed-actions">
<button class="preview-btn" @click.stop="previewContract(contract)"> <button class="preview-btn" @click.stop="previewContract(contract)">
@ -218,22 +218,22 @@ export default {
async goToSign(contract) { async goToSign(contract) {
try { try {
console.log('开始获取签署配置:', contract) console.log('开始获取签署配置:', contract)
// //
const response = await apiRoute.get('/contract/signForm', { const response = await apiRoute.get('/contract/signForm', {
contract_id: contract.contract_id contract_id: contract.id
}) })
console.log('获取签署配置响应:', response) console.log('获取签署配置响应:', response)
if (response.code === 1) { if (response.code === 1) {
const formConfig = response.data const formConfig = response.data
console.log('签署配置获取成功:', formConfig) console.log('签署配置获取成功:', formConfig)
// URL // URL
const jumpUrl = `/pages-common/contract/staff-contract-sign?contract_id=${contract.contract_id}&contract_name=${encodeURIComponent(contract.contract_name)}&personnel_id=${this.getMemberId()}` const jumpUrl = `/pages-common/contract/staff-contract-sign?contract_id=${contract.contract_id}&contract_name=${encodeURIComponent(contract.contract_name)}&personnel_id=${this.getMemberId()}`
console.log('准备跳转到:', jumpUrl) console.log('准备跳转到:', jumpUrl)
// //
uni.navigateTo({ uni.navigateTo({
url: jumpUrl, url: jumpUrl,
@ -264,7 +264,7 @@ export default {
}) })
} }
}, },
// IDID // IDID
getMemberId() { getMemberId() {
const userInfo = uni.getStorageSync('userInfo') const userInfo = uni.getStorageSync('userInfo')
@ -284,10 +284,10 @@ export default {
const response = await apiRoute.get('/contract/detail', { const response = await apiRoute.get('/contract/detail', {
id: contract.id id: contract.id
}) })
if (response.code === 1) { if (response.code === 1) {
const contractData = response.data const contractData = response.data
// //
uni.navigateTo({ uni.navigateTo({
url: `/pages-common/contract/contract_preview?contract_id=${contract.contract_id}&contract_name=${encodeURIComponent(contract.contract_name)}&sign_file=${encodeURIComponent(contractData.sign_file || '')}&contract_template=${encodeURIComponent(contractData.contract_template || '')}` url: `/pages-common/contract/contract_preview?contract_id=${contract.contract_id}&contract_name=${encodeURIComponent(contract.contract_name)}&sign_file=${encodeURIComponent(contractData.sign_file || '')}&contract_template=${encodeURIComponent(contractData.contract_template || '')}`
@ -311,20 +311,20 @@ export default {
async downloadContract(contract) { async downloadContract(contract) {
try { try {
uni.showLoading({ title: '获取下载链接...' }) uni.showLoading({ title: '获取下载链接...' })
const response = await apiRoute.get('/contract/download', { const response = await apiRoute.get('/contract/download', {
contract_id: contract.contract_id contract_id: contract.contract_id
}) })
if (response.code === 1) { if (response.code === 1) {
const downloadData = response.data const downloadData = response.data
// URL // URL
let downloadUrl = downloadData.download_url let downloadUrl = downloadData.download_url
if (downloadUrl && !downloadUrl.startsWith('http')) { if (downloadUrl && !downloadUrl.startsWith('http')) {
downloadUrl = 'http://localhost:20080' + (downloadUrl.startsWith('/') ? downloadUrl : '/' + downloadUrl) downloadUrl = 'http://localhost:20080' + (downloadUrl.startsWith('/') ? downloadUrl : '/' + downloadUrl)
} }
if (downloadUrl) { if (downloadUrl) {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
// //
@ -356,7 +356,7 @@ export default {
} }
}) })
// #endif // #endif
// #ifndef MP-WEIXIN // #ifndef MP-WEIXIN
// H5 // H5
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@ -618,4 +618,4 @@ export default {
transform: translateY(0); transform: translateY(0);
} }
} }
</style> </style>

73
uniapp/pages-common/contract/staff-contract-sign.vue

@ -36,7 +36,7 @@
<view class="form_title">请填写以下信息</view> <view class="form_title">请填写以下信息</view>
<view class="form_content"> <view class="form_content">
<view <view
v-for="(field, index) in partyBFormFields" v-for="(field, index) in allFormFields"
:key="index" :key="index"
class="form_field" class="form_field"
> >
@ -81,29 +81,29 @@
<!-- 文本输入框 --> <!-- 文本输入框 -->
<view v-else-if="field.field_type === 'text'" class="field_input"> <view v-else-if="field.field_type === 'text'" class="field_input">
<input <input
type="text" type="text"
:placeholder="field.placeholder || '请输入' + field.name" :placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]" v-model="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'" :disabled="field.sign_party !== 'party_b'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''" :class="field.sign_party !== 'party_b' ? 'disabled' : ''"
/> />
</view> </view>
<!-- 日期选择器 --> <!-- 日期选择器 -->
<view v-else-if="field.field_type === 'date'" class="field_input"> <view v-else-if="field.field_type === 'date'" class="field_input">
<picker <picker
mode="date" mode="date"
:value="formData[field.placeholder]" :value="formData[field.placeholder]"
@change="handleDateChange(field.placeholder, $event)" @change="handleDateChange(field.placeholder, $event)"
:disabled="field.data_type !== 'user_input'" :disabled="field.sign_party !== 'party_b'"
> >
<input <input
type="text" type="text"
:placeholder="field.placeholder || '请选择' + field.name" :placeholder="field.placeholder || '请选择' + field.name"
:value="formData[field.placeholder]" :value="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'" :disabled="field.sign_party !== 'party_b'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''" :class="field.sign_party !== 'party_b' ? 'disabled' : ''"
readonly readonly
/> />
</picker> </picker>
@ -111,22 +111,22 @@
<!-- 数字输入框 --> <!-- 数字输入框 -->
<view v-else-if="field.field_type === 'number'" class="field_input"> <view v-else-if="field.field_type === 'number'" class="field_input">
<input <input
type="number" type="number"
:placeholder="field.placeholder || '请输入' + field.name" :placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]" v-model="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'" :disabled="field.sign_party !== 'party_b'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''" :class="field.sign_party !== 'party_b' ? 'disabled' : ''"
/> />
</view> </view>
<!-- 文本域 --> <!-- 文本域 -->
<view v-else-if="field.field_type === 'textarea'" class="field_textarea"> <view v-else-if="field.field_type === 'textarea'" class="field_textarea">
<textarea <textarea
:placeholder="field.placeholder || '请输入' + field.name" :placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]" v-model="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'" :disabled="field.sign_party !== 'party_b'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''" :class="field.sign_party !== 'party_b' ? 'disabled' : ''"
></textarea> ></textarea>
</view> </view>
@ -192,8 +192,13 @@ export default {
return content return content
}, },
// party_b // party_a
partyBFormFields() { allFormFields() {
return this.formFields
},
// party_b
editableFormFields() {
return this.formFields.filter(field => field.sign_party === 'party_b') return this.formFields.filter(field => field.sign_party === 'party_b')
} }
}, },
@ -239,9 +244,7 @@ export default {
console.log('加载员工签署表单:', { contractId: this.contractId, personnelId: this.personnelId }) console.log('加载员工签署表单:', { contractId: this.contractId, personnelId: this.personnelId })
// //
const response = await apiRoute.get('/contract/signForm', { const response = await apiRoute.getSignForm(this.contractId)
contract_id: this.contractId
})
if (response.code === 1) { if (response.code === 1) {
const data = response.data const data = response.data
@ -281,15 +284,11 @@ export default {
initFormData() { initFormData() {
const data = {} const data = {}
// party_b //
this.partyBFormFields.forEach(field => { this.allFormFields.forEach(field => {
const key = field.placeholder || field.name const key = field.placeholder || field.name
// // 使
if (field.data_type === 'database' || field.data_type === 'system') { data[key] = field.default_value || ''
data[key] = field.default_value || ''
} else {
data[key] = field.default_value || ''
}
}) })
this.formData = data this.formData = data
}, },
@ -407,7 +406,7 @@ export default {
validateForm() { validateForm() {
// party_b // party_b
for (const field of this.partyBFormFields) { for (const field of this.editableFormFields) {
if (field.is_required) { if (field.is_required) {
const key = field.placeholder || field.name const key = field.placeholder || field.name
const value = this.formData[key] const value = this.formData[key]
@ -472,8 +471,8 @@ export default {
}, },
getSignatureImage() { getSignatureImage() {
// signatureparty_b // signature
for (const field of this.partyBFormFields) { for (const field of this.editableFormFields) {
if (field.data_type === 'signature') { if (field.data_type === 'signature') {
const key = field.placeholder || field.name const key = field.placeholder || field.name
return this.formData[key] || '' return this.formData[key] || ''

Loading…
Cancel
Save