Browse Source

修改 bug

develop
王泽彦 4 months ago
parent
commit
b354e07865
  1. 207
      admin/src/app/views/campus/campus.vue
  2. 12
      admin/src/app/views/contract/contract.vue
  3. 368
      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. 42
      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. 2
      uniapp/pages-common/contract/my_contract.vue
  16. 49
      uniapp/pages-common/contract/staff-contract-sign.vue

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

@ -147,9 +147,12 @@
<el-input v-model="sealDialog.form.campus_name" disabled />
</el-form-item>
<el-form-item label="签章图片" required>
<div class="seal-upload-container">
<!-- 无图片时显示上传组件 -->
<el-upload
v-if="sealDialog.fileList.length === 0"
ref="uploadRef"
class="upload-demo"
class="seal-upload"
:action="uploadAction"
:headers="uploadHeaders"
:before-upload="beforeUpload"
@ -159,14 +162,45 @@
list-type="picture-card"
:limit="1"
accept=".jpg,.jpeg,.png,.gif"
:show-file-list="false"
>
<el-icon><Plus /></el-icon>
<div class="upload-placeholder">
<el-icon class="upload-icon"><Plus /></el-icon>
<div class="upload-text">上传签章</div>
</div>
<template #tip>
<div class="el-upload__tip">
只能上传jpg/png/gif文件且不超过2MB
<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>
</div>
</el-form-item>
</el-form>
<template #footer>
@ -194,7 +228,7 @@ import { useDictionary } from '@/app/api/dict'
import { getCampusList, deleteCampus, uploadCampusSeal } from '@/app/api/campus'
import { img } from '@/utils/common'
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 { useRoute } from 'vue-router'
import { getToken } from '@/utils/common'
@ -318,8 +352,19 @@ const resetForm = (formEl: FormInstance | undefined) => {
const uploadSealEvent = (data: any) => {
sealDialog.form.campus_id = data.id
sealDialog.form.campus_name = data.campus_name
sealDialog.form.seal_image = ''
sealDialog.form.seal_image = data.seal_image || ''
//
if (data.seal_image) {
sealDialog.fileList = [{
name: '签章图片',
url: data.seal_image,
uid: Date.now()
}]
} else {
sealDialog.fileList = []
}
sealDialog.visible = true
}
@ -347,6 +392,14 @@ const beforeUpload = (file: any) => {
const onUploadSuccess = (response: any, file: any) => {
if (response.code === 1) {
sealDialog.form.seal_image = response.data.url
//
sealDialog.fileList = [{
name: file.name || '签章图片',
url: response.data.url,
uid: file.uid || Date.now()
}]
ElMessage.success('图片上传成功')
} else {
ElMessage.error(response.msg || '上传失败')
@ -362,26 +415,31 @@ const onUploadError = (error: any) => {
sealDialog.fileList = []
}
/**
* 移除文件回调
*/
const onRemoveFile = () => {
sealDialog.form.seal_image = ''
sealDialog.fileList = []
}
/**
* 保存签章
*/
const saveSeal = async () => {
if (!sealDialog.form.seal_image) {
ElMessage.warning('请先上传签章图片')
return
}
sealDialog.loading = true
try {
await uploadCampusSeal({
campus_id: sealDialog.form.campus_id,
seal_image: sealDialog.form.seal_image
seal_image: sealDialog.form.seal_image || null
})
sealDialog.visible = false
loadCampusList() //
ElMessage.success('签章保存成功')
} catch (error) {
console.error('签章上传失败:', error)
console.error('签章保存失败:', error)
ElMessage.error('签章保存失败')
} finally {
sealDialog.loading = false
}
@ -399,47 +457,116 @@ const saveSeal = async () => {
-webkit-box-orient: vertical;
}
/* 修复上传组件在弹窗中的样式 */
.upload-demo {
/* 签章上传容器样式 */
.seal-upload-container {
width: 100%;
}
:deep(.el-upload-list--picture-card) {
--el-upload-list-picture-card-size: 100px;
display: flex;
flex-wrap: wrap;
margin: 0 0 10px 0;
}
/* 上传组件样式 */
.seal-upload {
width: 100%;
:deep(.el-upload--picture-card) {
--el-upload-picture-card-size: 100px;
width: var(--el-upload-picture-card-size);
height: var(--el-upload-picture-card-size);
line-height: calc(var(--el-upload-picture-card-size) - 2px);
margin-bottom: 10px;
width: 200px;
height: 120px;
border: 2px dashed var(--el-border-color-lighter);
border-radius: 8px;
background-color: var(--el-fill-color-blank);
transition: all 0.3s;
&:hover {
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
:deep(.el-upload-list__item) {
width: var(--el-upload-list-picture-card-size);
height: var(--el-upload-list-picture-card-size);
margin: 0 8px 10px 0;
.upload-placeholder {
display: flex;
flex-direction: column;
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);
}
/* 确保上传区域不超出弹窗 */
:deep(.el-upload-dragger) {
width: 100%;
height: 100px;
.upload-text {
font-size: 14px;
font-weight: 500;
}
}
/* 修复提示文字被盖住的问题 */
:deep(.el-upload__tip) {
margin-top: 25%;
line-height: 1.4;
margin-top: 12px;
text-align: center;
color: var(--el-text-color-regular);
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;
z-index: 1;
clear: both;
display: block;
display: inline-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>

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

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

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

@ -10,18 +10,19 @@
<div class="sign-content" v-loading="loading">
<!-- 合同基础信息 -->
<div class="contract-header" v-if="contractData">
<h2>{{ contractData.contract?.contract_name || '合同签署' }}</h2>
<h2>{{ contractData.contract_id_name || '合同签署' }}</h2>
<div class="contract-meta">
<el-tag :type="getStatusType(contractData.status)" size="large">
{{ getStatusLabel(contractData.status) }}
</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>
</div>
</div>
<!-- 主要内容区域左右布局 -->
<div class="contract-main">
<div class="contract-content-wrapper">
<!-- 左侧合同内容 -->
<div class="contract-content-panel">
<div class="panel-header">
@ -107,8 +108,8 @@
<el-tag size="small" type="info">系统</el-tag>
</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">
<el-image
:src="field.display_value"
@ -116,9 +117,41 @@
:preview-src-list="[field.display_value]"
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>
<span v-else class="no-signature">未签名</span>
</div>
<!-- 字段信息 -->
@ -140,6 +173,7 @@
</div>
</div>
</div>
</div>
<!-- 签名区域 -->
<div class="signature-section" v-if="contractData?.status === 1">
@ -206,6 +240,13 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button
type="success"
:loading="saving"
@click="handleSaveFields"
>
保存字段
</el-button>
<el-button
v-if="contractData?.status === 1"
type="primary"
@ -224,7 +265,8 @@
import { ref, computed, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
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 {
show: boolean
@ -233,7 +275,7 @@ interface Props {
}
const props = defineProps<Props>()
const emit = defineEmits(['update:show', 'success'])
const emit = defineEmits(['update:show', 'success', 'fieldsSaved'])
//
const showDialog = computed({
@ -243,6 +285,7 @@ const showDialog = computed({
const loading = ref(false)
const signing = ref(false)
const saving = ref(false)
const signMethod = ref('type') // 'type' | 'upload'
const signatureImage = ref('')
const isDrawing = ref(false)
@ -251,6 +294,8 @@ const currentStroke = ref<{x: number, y: number}[]>([])
const signatureCanvas = ref<HTMLCanvasElement>()
const formRef = ref()
const contentDisplay = ref<HTMLElement>()
const fileInputs = ref<Record<string, HTMLInputElement>>({})
const currentUploadField = ref<any>(null)
//
const fullContractData = ref<any>(null)
@ -456,8 +501,8 @@ const getFieldTypeLabel = (dataType: string) => {
'user_input': '用户输入',
'database': '数据库',
'system': '系统函数',
'signature': '手写签名',
'sign_img': '签名图片'
'signature': '电子签名',
'sign_img': '印章图片'
}
return labels[dataType as keyof typeof labels] || dataType
}
@ -468,7 +513,7 @@ const getFieldTypeColor = (dataType: string) => {
'database': 'success',
'system': 'info',
'signature': 'warning',
'sign_img': 'warning'
'sign_img': 'success'
}
return colors[dataType as keyof typeof colors] || 'info'
}
@ -541,25 +586,55 @@ const refreshContent = () => {
//
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 = {}
formRules.value = {}
// 使fill_data使field_configsvalue
const savedFillData = fullContractData.value.fill_data || {}
fullContractData.value.field_configs.forEach((field: any) => {
//
if (field.is_editable && field.data_type === 'user_input') {
formData.value[field.placeholder] = field.value || field.default_value || ''
// 使
if (field.is_editable) {
// 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] = ''
}
//
if (field.is_required && field.is_editable) {
console.log(`字段 ${field.placeholder} 初始化值:`, formData.value[field.placeholder])
}
// -
if (field.is_required && field.is_editable && field.data_type === 'user_input') {
formRules.value[field.placeholder] = [
{ required: true, message: `请填写${field.placeholder}`, trigger: 'blur' }
]
}
})
console.log('表单数据初始化完成:', formData.value)
//
updateProcessedContent()
}
@ -635,6 +710,10 @@ const loadFullContractData = async () => {
if (response.data) {
fullContractData.value = response.data
//
await nextTick()
initFormData()
//
updateProcessedContent()
} 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) => {
if (visible) {
@ -657,7 +916,7 @@ watch(() => props.show, (visible) => {
currentStroke.value = []
fullContractData.value = null
//
// (initFormData)
loadFullContractData()
//
@ -666,15 +925,6 @@ watch(() => props.show, (visible) => {
})
}
})
//
watch(() => props.contractData, (newData) => {
if (newData && props.show) {
nextTick(() => {
initFormData()
})
}
}, { deep: true })
</script>
<style lang="scss" scoped>
@ -700,13 +950,21 @@ watch(() => props.contractData, (newData) => {
:deep(.el-dialog__body) {
padding: 0;
max-height: 85vh;
overflow-y: auto;
max-height: 75vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
.sign-content {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
.contract-header {
flex-shrink: 0;
padding: 24px 32px;
background: #fafbfc;
border-bottom: 1px solid #f0f0f0;
@ -741,11 +999,12 @@ watch(() => props.contractData, (newData) => {
}
}
.contract-main {
.contract-content-wrapper {
flex: 1;
display: flex;
gap: 24px;
padding: 24px 32px;
min-height: 500px;
min-height: 0;
overflow: hidden;
.contract-content-panel {
flex: 1;
@ -909,11 +1168,32 @@ watch(() => props.contractData, (newData) => {
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;
}
}
@ -935,8 +1215,10 @@ watch(() => props.contractData, (newData) => {
}
}
}
}
.signature-section {
flex-shrink: 0;
padding: 24px 32px;
background: #f8fafc;
border-top: 1px solid #e5e7eb;
@ -1026,7 +1308,6 @@ watch(() => props.contractData, (newData) => {
}
}
}
}
.dialog-footer {
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,
.fields-form {
&::-webkit-scrollbar {

22
admin/src/utils/common.ts

@ -143,6 +143,28 @@ export function assetImg(path: string) {
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

98
admin/src/utils/directUpload.ts

@ -336,6 +336,104 @@ export async function uploadFile(options: UploadOptions): Promise<UploadResult>
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([
["contract_id",0],
["personnel_id",0],
["fill_data",""],
["sign_time",""],
["signature_image",""],
["status",1]
]);
$this->validate($data, 'app\validate\contract_sign\ContractSign.edit');

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

@ -187,6 +187,48 @@ class Contract extends BaseApiService
}
}
/**
* 获取合同签署表单(根据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

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

@ -301,13 +301,13 @@ class ContractDistributionService extends BaseAdminService
private function copyDataSourceConfig(int $contractId, int $contractSignId): bool
{
try {
// 获取原合同的模板配置(contract_sign_id 为 null 的记录)
$sourceConfigs = Db::table('school_document_data_source_config')
->where('contract_id', $contractId)
->where('contract_sign_id', null)
->select();
// 从 school_contract 表获取占位符配置
$contract = Db::table('school_contract')
->where('id', $contractId)
->field('placeholder_config')
->find();
if (empty($sourceConfigs)) {
if (empty($contract) || empty($contract['placeholder_config'])) {
// 如果没有模板配置,记录日志但不阻止分发流程
Log::info('合同无数据源配置模板', [
'contract_id' => $contractId,
@ -316,23 +316,34 @@ class ContractDistributionService extends BaseAdminService
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');
$insertData = [];
foreach ($sourceConfigs as $config) {
// 为每个占位符创建配置记录
foreach ($placeholderConfig as $placeholder => $config) {
$insertData[] = [
'contract_id' => $config['contract_id'],
'contract_id' => $contractId,
'contract_sign_id' => $contractSignId,
'placeholder' => $config['placeholder'],
'data_type' => $config['data_type'],
'table_name' => $config['table_name'],
'field_name' => $config['field_name'],
'field_type' => $config['field_type'],
'is_required' => $config['is_required'],
'default_value' => $config['default_value'],
'system_function' => $config['system_function'],
'sign_party' => $config['sign_party'],
'validation_rule' => $config['validation_rule'],
'placeholder' => $placeholder,
'data_type' => $config['data_type'] ?? 'user_input',
'table_name' => $config['table_name'] ?? '',
'field_name' => $config['field_name'] ?? '',
'field_type' => $config['field_type'] ?? 'text',
'is_required' => $config['is_required'] ?? 0,
'default_value' => $config['default_value'] ?? '',
'system_function' => $config['system_function'] ?? '',
'sign_party' => $config['sign_party'] ?? '',
'validation_rule' => $config['validation_rule'] ?? '',
'created_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)
{
// 处理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);
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['file_size_formatted'] = $this->formatFileSize($info['file_size']);
// 获取数据源配置信息,从school_document_data_source_config表获取
// 基于placeholder_config构建数据源配置,不查询school_document_data_source_config表
$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[] = [
'id' => $config['id'],
'placeholder' => $config['placeholder'],
'id' => 0,
'placeholder' => $placeholder,
'data_type' => $config['data_type'] ?? 'user_input',
'table_name' => $config['table_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;
}
@ -448,12 +417,17 @@ class DocumentTemplateService extends BaseAdminService
try {
// 1. 保存配置到合同表的placeholder_config字段(保持兼容性)
$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->save();
// 2. 同时保存到独立的数据源配置表(用户期望的表)
$this->saveConfigToDataSourceTable($templateId, $configData);
\think\facade\Db::commit();
return true;
} catch (\Exception $e) {
@ -1458,10 +1432,15 @@ class DocumentTemplateService extends BaseAdminService
// 更新占位符配置
if (!empty($newConfig)) {
$template->placeholder_config = json_encode($newConfig);
$template->save();
// 同步更新数据源配置表
$this->saveConfigToDataSourceTable($id, $newConfig);
// 更新placeholder字段为占位符的逗号分隔文本
$placeholders = array_keys($newConfig);
$template->placeholder = implode(',', $placeholders);
// 更新placeholders字段为JSON数组
$template->placeholders = json_encode($placeholders);
$template->save();
}
return [
@ -1584,10 +1563,15 @@ class DocumentTemplateService extends BaseAdminService
// 更新占位符配置
if (!empty($newConfig)) {
$template->placeholder_config = json_encode($newConfig);
$template->save();
// 同步更新数据源配置表
$this->saveConfigToDataSourceTable($id, $newConfig);
// 更新placeholder字段为占位符的逗号分隔文本
$placeholders = array_keys($newConfig);
$template->placeholder = implode(',', $placeholders);
// 更新placeholders字段为JSON数组
$template->placeholders = json_encode($placeholders);
$template->save();
}
return [

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

@ -169,8 +169,11 @@ class PersonnelService extends BaseAdminService
$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['native_place']);
$this->model->where([['id', '=', $id]])->update($data);
(new PersonnelInfo())->where(['person_id' => $id])->update($info);
Db::commit();

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

@ -346,13 +346,12 @@ class ContractService extends BaseApiService
// 查询合同签署记录
$contractSign = ContractSign::alias('cs')
->join('school_contract c', 'cs.contract_id = c.id')
->where('cs.contract_id', $contract_id)
->where('cs.personnel_id', $personnel_id)
->where('cs.type', 1) // 员工合同
->where('cs.id', $contract_id)
->where('cs.deleted_at', 0)
->where('c.deleted_at', 0)
->field([
'cs.id as sign_id',
'cs.contract_id',
'cs.status',
'cs.fill_data',
'c.contract_name',
@ -371,7 +370,7 @@ class ContractService extends BaseApiService
// 获取表单字段配置
$formFields = Db::table('school_document_data_source_config')
->where('contract_id', $contract_id)
->where('contract_sign_id', $contract_id)
->order('id', 'asc')
->select()
->toArray();
@ -397,7 +396,7 @@ class ContractService extends BaseApiService
'msg' => '获取成功',
'data' => [
'sign_id' => $contractData['sign_id'],
'contract_id' => $contract_id,
'contract_id' => $contractData['contract_id'],
'contract_name' => $contractData['contract_name'],
'contract_type' => $contractData['contract_type'],
'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 })
},
// 获取合同签署表单配置
async getSignForm(contractId) {
return await http.get('/contract/signForm', { contract_id: contractId })
},
// 获取合同表单字段(暂时返回空,需要后端实现)
async getContractFormFields(contractId) {
return { code: 1, data: [] }

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

@ -221,7 +221,7 @@ export default {
//
const response = await apiRoute.get('/contract/signForm', {
contract_id: contract.contract_id
contract_id: contract.id
})
console.log('获取签署配置响应:', response)

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

@ -36,7 +36,7 @@
<view class="form_title">请填写以下信息</view>
<view class="form_content">
<view
v-for="(field, index) in partyBFormFields"
v-for="(field, index) in allFormFields"
:key="index"
class="form_field"
>
@ -85,8 +85,8 @@
type="text"
:placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''"
:disabled="field.sign_party !== 'party_b'"
:class="field.sign_party !== 'party_b' ? 'disabled' : ''"
/>
</view>
@ -96,14 +96,14 @@
mode="date"
:value="formData[field.placeholder]"
@change="handleDateChange(field.placeholder, $event)"
:disabled="field.data_type !== 'user_input'"
:disabled="field.sign_party !== 'party_b'"
>
<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' : ''"
:disabled="field.sign_party !== 'party_b'"
:class="field.sign_party !== 'party_b' ? 'disabled' : ''"
readonly
/>
</picker>
@ -115,8 +115,8 @@
type="number"
:placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''"
:disabled="field.sign_party !== 'party_b'"
:class="field.sign_party !== 'party_b' ? 'disabled' : ''"
/>
</view>
@ -125,8 +125,8 @@
<textarea
:placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''"
:disabled="field.sign_party !== 'party_b'"
:class="field.sign_party !== 'party_b' ? 'disabled' : ''"
></textarea>
</view>
@ -192,8 +192,13 @@ export default {
return content
},
// party_b
partyBFormFields() {
// party_a
allFormFields() {
return this.formFields
},
// party_b
editableFormFields() {
return this.formFields.filter(field => field.sign_party === 'party_b')
}
},
@ -239,9 +244,7 @@ export default {
console.log('加载员工签署表单:', { contractId: this.contractId, personnelId: this.personnelId })
//
const response = await apiRoute.get('/contract/signForm', {
contract_id: this.contractId
})
const response = await apiRoute.getSignForm(this.contractId)
if (response.code === 1) {
const data = response.data
@ -281,15 +284,11 @@ export default {
initFormData() {
const data = {}
// party_b
this.partyBFormFields.forEach(field => {
//
this.allFormFields.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
},
@ -407,7 +406,7 @@ export default {
validateForm() {
// party_b
for (const field of this.partyBFormFields) {
for (const field of this.editableFormFields) {
if (field.is_required) {
const key = field.placeholder || field.name
const value = this.formData[key]
@ -472,8 +471,8 @@ export default {
},
getSignatureImage() {
// signatureparty_b
for (const field of this.partyBFormFields) {
// signature
for (const field of this.editableFormFields) {
if (field.data_type === 'signature') {
const key = field.placeholder || field.name
return this.formData[key] || ''

Loading…
Cancel
Save