Browse Source

修改 bug

master
王泽彦 8 months ago
parent
commit
676c88eb0b
  1. 32
      niucloud/app/api/controller/apiController/Contract.php
  2. 1
      niucloud/app/api/route/route.php
  3. 238
      niucloud/app/service/api/apiService/ContractService.php
  4. 4
      uniapp/common/config.js
  5. 10
      uniapp/pages-common/contract/contract_form.vue
  6. 70
      uniapp/pages-common/contract/contract_sign.vue
  7. 220
      uniapp/pages-common/contract/my_contract.vue
  8. 829
      uniapp/pages-common/contract/staff-contract-sign.vue
  9. 10
      uniapp/pages.json
  10. 2
      uniapp/pages/common/profile/index.vue

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

@ -152,6 +152,38 @@ class Contract extends BaseApiService
}
}
/**
* 获取员工合同签署表单配置
* @param Request $request
* @return mixed
*/
public function getSignForm(Request $request)
{
$contract_id = $request->param('contract_id', 0);
if (empty($contract_id)) {
return fail('合同ID不能为空');
}
$where = [
'contract_id' => $contract_id,
'personnel_id' => $this->member_id
];
try {
$service = new ContractService();
$res = $service->getStaffContractSignForm($where);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data']);
} catch (\Exception $e) {
return fail('获取签署表单失败:' . $e->getMessage());
}
}
/**
* 下载合同文件
* @param Request $request

1
niucloud/app/api/route/route.php

@ -434,6 +434,7 @@ Route::group(function () {
//合同管理
Route::get('contract/myContracts', 'apiController.Contract/myContracts');
Route::get('contract/detail', 'apiController.Contract/detail');
Route::get('contract/signForm', 'apiController.Contract/getSignForm'); // 获取签署表单配置
Route::post('contract/sign', 'apiController.Contract/sign');
Route::get('contract/signStatus', 'apiController.Contract/signStatus');
Route::get('contract/download', 'apiController.Contract/download');

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

@ -49,10 +49,12 @@ class ContractService extends BaseApiService
}
// 查询合同签订记录,关联合同表
// type=1是给员工签的合同,关联的是school_personnel表
$contractSignModel = new ContractSign();
$list = $contractSignModel->alias('cs')
->join('school_contract c', 'cs.contract_id = c.id')
->where('cs.personnel_id', $personnel_id)
->where('cs.type', 1) // 添加type=1条件,确保查询员工合同
->where('cs.deleted_at', 0)
->where('c.deleted_at', 0)
->field([
@ -114,6 +116,7 @@ class ContractService extends BaseApiService
$contractSign = ContractSign::alias('cs')
->join('school_contract c', 'cs.contract_id = c.id')
->where('cs.id', $id)
->where('cs.type', 1) // 添加type=1条件,确保查询员工合同
->where('cs.deleted_at', 0)
->where('c.deleted_at', 0)
->field([
@ -169,9 +172,11 @@ class ContractService extends BaseApiService
try {
$contract_id = $data['contract_id'] ?? 0;
$personnel_id = $data['personnel_id'] ?? 0;
$sign_file = $data['sign_file'] ?? '';
$form_data = $data['form_data'] ?? [];
$signature_image = $data['signature_image'] ?? '';
$sign_file = $data['sign_file'] ?? ''; // 兼容旧版本
if (empty($contract_id) || empty($personnel_id) || empty($sign_file)) {
if (empty($contract_id) || empty($personnel_id)) {
$res['msg'] = '参数错误';
return $res;
}
@ -182,6 +187,7 @@ class ContractService extends BaseApiService
// 查询合同签订记录
$contractSign = ContractSign::where('contract_id', $contract_id)
->where('personnel_id', $personnel_id)
->where('type', 1) // 添加type=1条件,确保查询员工合同
->where('deleted_at', 0)
->find();
@ -205,21 +211,30 @@ class ContractService extends BaseApiService
return $res;
}
// 验证必填字段(如果有表单数据)
if ($form_data) {
$this->validateStaffFormData($contract_id, $form_data);
}
// 生成签署后的合同文档
$generatedFile = null;
if ($sign_file) {
$generatedFile = $this->generateStaffSignedContract($contract_id, $personnel_id, $sign_file);
$useSignatureImage = $signature_image ?: $sign_file; // 优先使用新的signature_image字段
if ($useSignatureImage || $form_data) {
$generatedFile = $this->generateStaffSignedContract($contract_id, $personnel_id, $form_data, $useSignatureImage);
}
// 更新签订信息
$updateData = [
'sign_file' => $generatedFile ?: $sign_file,
'status' => 2, // 已签署
'sign_file' => $generatedFile ?: $useSignatureImage,
'sign_time' => date('Y-m-d H:i:s'),
'fill_data' => $form_data ? json_encode($form_data, JSON_UNESCAPED_UNICODE) : null,
'updated_at' => date('Y-m-d H:i:s')
];
if ($generatedFile) {
$updateData['signature_image'] = $sign_file;
if ($useSignatureImage) {
$updateData['signature_image'] = $useSignatureImage;
}
$updateResult = ContractSign::where('id', $contractSign['id'])->update($updateData);
@ -237,6 +252,7 @@ class ContractService extends BaseApiService
'code' => 1,
'msg' => '签订成功',
'data' => [
'sign_id' => $contractSign['id'],
'contract_id' => $contract_id,
'sign_time' => $updateData['sign_time'],
'generated_file' => $generatedFile
@ -275,6 +291,7 @@ class ContractService extends BaseApiService
$contractSign = ContractSign::where('contract_id', $contract_id)
->where('personnel_id', $personnel_id)
->where('type', 1) // 添加type=1条件,确保查询员工合同
->where('deleted_at', 0)
->field('id,sign_file,status,sign_time')
->find();
@ -303,6 +320,98 @@ class ContractService extends BaseApiService
return $res;
}
/**
* 获取员工合同签署表单配置
* @param array $where
* @return array
*/
public function getStaffContractSignForm(array $where)
{
$res = [
'code' => 0,
'msg' => '获取签署表单失败',
'data' => []
];
try {
$contract_id = $where['contract_id'] ?? 0;
$personnel_id = $where['personnel_id'] ?? 0;
if (empty($contract_id) || empty($personnel_id)) {
$res['msg'] = '参数错误';
return $res;
}
// 查询合同签署记录
$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.deleted_at', 0)
->where('c.deleted_at', 0)
->field([
'cs.id as sign_id',
'cs.status',
'cs.fill_data',
'c.contract_name',
'c.contract_type',
'c.contract_content',
'c.contract_template'
])
->find();
if (!$contractSign) {
$res['msg'] = '合同不存在或无权限访问';
return $res;
}
$contractData = $contractSign->toArray();
// 获取表单字段配置
$formFields = Db::table('school_document_data_source_config')
->where('contract_id', $contract_id)
->order('id', 'asc')
->select()
->toArray();
// 处理表单字段
$processedFields = [];
foreach ($formFields as $field) {
$processedFields[] = [
'name' => str_replace(['{{', '}}'], '', $field['placeholder']),
'placeholder' => str_replace(['{{', '}}'], '', $field['placeholder']),
'field_type' => $field['field_type'] ?: 'text',
'data_type' => $field['data_type'],
'is_required' => $field['is_required'] ? true : false,
'default_value' => $field['default_value'] ?: '',
'sign_party' => $field['sign_party'] ?: null
];
}
$res = [
'code' => 1,
'msg' => '获取成功',
'data' => [
'sign_id' => $contractData['sign_id'],
'contract_id' => $contract_id,
'contract_name' => $contractData['contract_name'],
'contract_type' => $contractData['contract_type'],
'contract_content' => $contractData['contract_content'] ?: '',
'form_fields' => $processedFields,
'contract_template_url' => $contractData['contract_template'] ? '/upload/' . $contractData['contract_template'] : '',
'status' => $contractData['status'],
'existing_data' => $contractData['fill_data'] ? json_decode($contractData['fill_data'], true) : []
]
];
} catch (\Exception $e) {
$res['msg'] = '获取签署表单异常:' . $e->getMessage();
}
return $res;
}
/**
* 下载合同文件
* @param array $where
@ -330,6 +439,7 @@ class ContractService extends BaseApiService
->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) // 添加type=1条件,确保查询员工合同
->where('cs.deleted_at', 0)
->where('c.deleted_at', 0)
->field([
@ -367,14 +477,40 @@ class ContractService extends BaseApiService
return $res;
}
/**
* 验证员工表单数据
* @param int $contractId
* @param array $formData
* @throws \Exception
*/
private function validateStaffFormData($contractId, $formData)
{
// 获取数据源配置
$configs = Db::table('school_document_data_source_config')
->where('contract_id', $contractId)
->where('is_required', 1)
->select()
->toArray();
foreach ($configs as $config) {
$placeholder = str_replace(['{{', '}}'], '', $config['placeholder']);
$value = $formData[$placeholder] ?? '';
if (empty($value)) {
throw new \Exception("必填字段 {$placeholder} 不能为空");
}
}
}
/**
* 生成员工签署后的合同文档
* @param int $contractId
* @param int $personnelId
* @param array $formData
* @param string $signatureImage
* @return string|null
*/
private function generateStaffSignedContract($contractId, $personnelId, $signatureImage)
private function generateStaffSignedContract($contractId, $personnelId, $formData = [], $signatureImage = '')
{
try {
// 获取合同模板信息
@ -401,7 +537,7 @@ class ContractService extends BaseApiService
}
// 获取员工信息并准备填充数据
$fillData = $this->prepareStaffFillData($contractId, $personnelId);
$fillData = $this->prepareStaffFillData($contractId, $personnelId, $formData);
// 使用PhpWord处理模板
$templateProcessor = new TemplateProcessor($templatePath);
@ -440,13 +576,44 @@ class ContractService extends BaseApiService
* 准备员工填充数据
* @param int $contractId
* @param int $personnelId
* @param array $formData
* @return array
*/
private function prepareStaffFillData($contractId, $personnelId)
private function prepareStaffFillData($contractId, $personnelId, $formData = [])
{
$fillData = [];
try {
// 获取数据源配置
$configs = Db::table('school_document_data_source_config')
->where('contract_id', $contractId)
->select()
->toArray();
foreach ($configs as $config) {
$placeholder = str_replace(['{{', '}}'], '', $config['placeholder']);
$value = '';
switch ($config['data_type']) {
case 'database':
$value = $this->getStaffDataFromDatabase($config['table_name'], $config['field_name'], $personnelId);
break;
case 'system':
$value = $this->getSystemValue($config['system_function']);
break;
case 'user_input':
case 'signature':
case 'sign_img':
default:
$value = $formData[$placeholder] ?? $config['default_value'] ?? '';
break;
}
$fillData[$placeholder] = $value;
}
// 如果没有配置,使用默认填充逻辑
if (empty($fillData)) {
// 获取员工信息
$personnel = Db::table('school_personnel')->where('id', $personnelId)->find();
if ($personnel) {
@ -462,6 +629,12 @@ class ContractService extends BaseApiService
$fillData['签署时间'] = date('Y-m-d H:i:s');
$fillData['合同编号'] = $contractId . date('Ymd') . $personnelId;
// 合并表单数据
if ($formData) {
$fillData = array_merge($fillData, $formData);
}
}
} catch (\Exception $e) {
// 记录错误日志但不中断流程
}
@ -469,6 +642,51 @@ class ContractService extends BaseApiService
return $fillData;
}
/**
* 从数据库获取员工数据
* @param string $tableName
* @param string $fieldName
* @param int $personnelId
* @return string
*/
private function getStaffDataFromDatabase($tableName, $fieldName, $personnelId)
{
try {
switch ($tableName) {
case 'school_personnel':
$data = Db::table('school_personnel')->where('id', $personnelId)->find();
return $data[$fieldName] ?? '';
default:
return '';
}
} catch (\Exception $e) {
return '';
}
}
/**
* 获取系统值
* @param string $systemFunction
* @return string
*/
private function getSystemValue($systemFunction)
{
switch ($systemFunction) {
case 'current_date':
return date('Y-m-d');
case 'current_time':
return date('Y-m-d H:i:s');
case 'current_year':
return date('Y');
case 'current_month':
return date('m');
case 'current_day':
return date('d');
default:
return '';
}
}
/**
* 处理员工签名图片
* @param string $signatureImage

4
uniapp/common/config.js

@ -1,6 +1,6 @@
// 环境变量配置
const env = 'development'
// const env = 'prod'
// const env = 'development'
const env = 'prod'
const isMockEnabled = false // 默认禁用Mock优先模式,仅作为回退
const isDebug = false // 默认启用调试模式
const devurl = 'http://localhost:20080/api'

10
uniapp/pages-common/contract/contract_form.vue

@ -120,13 +120,15 @@ export default {
return {
contractId: 0,
studentId: 0,
personnelId: 0,
contractName: '',
contractInfo: {},
formFields: [],
formData: {},
signatures: {},
submitting: false,
loading: false
loading: false,
isStaffMode: false //
}
},
@ -153,9 +155,13 @@ export default {
onLoad(options) {
this.contractId = parseInt(options.contract_id) || 0
this.studentId = parseInt(options.student_id) || 0
this.personnelId = parseInt(options.personnel_id) || 0
this.contractName = options.contractName ? decodeURIComponent(options.contractName) : '合同签署'
if (this.contractId && this.studentId) {
//
this.isStaffMode = this.personnelId > 0
if (this.contractId && (this.studentId > 0 || this.personnelId > 0)) {
this.loadSignForm()
} else {
uni.showToast({

70
uniapp/pages-common/contract/contract_sign.vue

@ -125,6 +125,8 @@ export default {
fieldName: '', //
fieldPlaceholder: '', //
isFormField: false, //
isStaff: false, //
personnelId: 0, // ID
canvas: null,
ctx: null,
isDrawing: false,
@ -147,16 +149,22 @@ export default {
}
},
onLoad(options) {
//
//
if (options.id) {
//
this.contractId = parseInt(options.id)
this.isFormField = false
}
if (options.contract_id) {
//
//
this.contractId = parseInt(options.contract_id)
this.isFormField = true
this.isFormField = !options.isStaff //
}
//
if (options.isStaff === 'true') {
this.isStaff = true
this.personnelId = parseInt(options.personnel_id) || 0
}
if (options.contractName) {
@ -373,6 +381,9 @@ export default {
if (this.isFormField) {
// -
this.returnSignatureData()
} else if (this.isStaff) {
// -
this.uploadStaffSignature()
} else {
// -
this.uploadSignature()
@ -433,6 +444,59 @@ export default {
}
},
//
async uploadStaffSignature() {
if (!this.signatureImageUrl) {
uni.showToast({
title: '签名生成失败,请重试',
icon: 'none'
})
return
}
this.submitting = true
try {
//
const uploadResult = await this.uploadSignatureFile()
if (!uploadResult.success) {
throw new Error(uploadResult.message || '上传签名文件失败')
}
//
const response = await apiRoute.post('/contract/sign', {
contract_id: this.contractId,
sign_file: uploadResult.url
})
if (response.code === 1) {
uni.showToast({
title: '合同签署成功',
icon: 'success',
duration: 2000
})
setTimeout(() => {
//
uni.navigateBack({
delta: 1
})
}, 2000)
} else {
throw new Error(response.msg || '合同签署失败')
}
} catch (error) {
console.error('员工端合同签署失败:', error)
uni.showToast({
title: error.message || '网络错误,请稍后重试',
icon: 'none'
})
} finally {
this.submitting = false
}
},
//
returnSignatureData() {
if (!this.signatureImageUrl) {

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

@ -69,10 +69,21 @@
</view>
<!-- 操作按钮 -->
<view class="contract-actions" v-if="needSignButton(contract)">
<button class="sign-btn" @click.stop="goToSign(contract)">
<view class="contract-actions">
<!-- 未签署状态显示签订按钮 -->
<button v-if="needSignButton(contract)" class="sign-btn" @click.stop="goToSign(contract)">
立即签订
</button>
<!-- 已签署状态显示预览和下载按钮 -->
<view v-else class="signed-actions">
<button class="preview-btn" @click.stop="previewContract(contract)">
预览合同
</button>
<button class="download-btn" @click.stop="downloadContract(contract)">
下载合同
</button>
</view>
</view>
</view>
</view>
@ -143,12 +154,17 @@ export default {
limit: this.pageSize
})
if (response.code === 1) {
console.log('API响应成功,完整数据:', response)
console.log('response.data:', response.data)
console.log('response.data.data:', response.data.data)
const newContracts = response.data.data || []
console.log('提取的合同数据:', newContracts)
if (refresh) {
this.contracts = newContracts
} else {
this.contracts = [...this.contracts, ...newContracts]
}
console.log('更新后的contracts:', this.contracts)
//
this.hasMore = newContracts.length === this.pageSize
@ -199,10 +215,60 @@ export default {
},
//
goToSign(contract) {
async goToSign(contract) {
try {
console.log('开始获取签署配置:', contract)
//
const response = await apiRoute.get('/contract/signForm', {
contract_id: contract.contract_id
})
console.log('获取签署配置响应:', response)
if (response.code === 1) {
const formConfig = response.data
console.log('签署配置获取成功:', formConfig)
// URL
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)
//
uni.navigateTo({
url: jumpUrl,
success: (res) => {
console.log('页面跳转成功:', res)
},
fail: (err) => {
console.error('页面跳转失败:', err)
uni.showToast({
title: '页面跳转失败: ' + JSON.stringify(err),
icon: 'none',
duration: 3000
})
}
})
} else {
console.error('获取签署配置失败:', response.msg)
uni.showToast({
title: response.msg || '获取签署配置失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取签署配置异常:', error)
//
uni.navigateTo({
url: `/pages-common/contract/contract_sign?id=${contract.id}&contractName=${encodeURIComponent(contract.contract_name)}`
url: `/pages-common/contract/contract_sign?contract_id=${contract.contract_id}&personnel_id=${this.getMemberId()}&contractName=${encodeURIComponent(contract.contract_name)}&isStaff=true`
})
}
},
// IDID
getMemberId() {
const userInfo = uni.getStorageSync('userInfo')
return userInfo ? userInfo.member_id || userInfo.id : 0
},
//
@ -211,6 +277,115 @@ export default {
},
//
async previewContract(contract) {
try {
//
const response = await apiRoute.get('/contract/detail', {
id: contract.id
})
if (response.code === 1) {
const contractData = response.data
//
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 || '')}`
})
} else {
uni.showToast({
title: response.msg || '获取合同信息失败',
icon: 'none'
})
}
} catch (error) {
console.error('预览合同失败:', error)
uni.showToast({
title: '预览合同失败',
icon: 'none'
})
}
},
//
async downloadContract(contract) {
try {
uni.showLoading({ title: '获取下载链接...' })
const response = await apiRoute.get('/contract/download', {
contract_id: contract.contract_id
})
if (response.code === 1) {
const downloadData = response.data
// URL
let downloadUrl = downloadData.download_url
if (downloadUrl && !downloadUrl.startsWith('http')) {
downloadUrl = 'http://localhost:20080' + (downloadUrl.startsWith('/') ? downloadUrl : '/' + downloadUrl)
}
if (downloadUrl) {
// #ifdef MP-WEIXIN
//
uni.downloadFile({
url: downloadUrl,
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
success: () => {
console.log('打开文档成功')
},
fail: (err) => {
console.error('打开文档失败:', err)
uni.showToast({
title: '文件打开失败',
icon: 'none'
})
}
})
}
},
fail: (err) => {
console.error('下载失败:', err)
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
})
// #endif
// #ifndef MP-WEIXIN
// H5
if (typeof window !== 'undefined') {
window.open(downloadUrl, '_blank')
}
// #endif
} else {
uni.showToast({
title: '文件链接不存在',
icon: 'none'
})
}
} else {
uni.showToast({
title: response.msg || '获取下载链接失败',
icon: 'none'
})
}
} catch (error) {
console.error('下载合同失败:', error)
uni.showToast({
title: '下载合同失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
},
//
formatDate(dateString) {
if (!dateString) return '-'
@ -347,7 +522,7 @@ export default {
.contract-actions {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f0f0f0;
border-top: 1rpx solid #444;
.sign-btn {
width: 100%;
@ -366,6 +541,41 @@ export default {
background-color: #22b39a;
}
}
.signed-actions {
display: flex;
gap: 16rpx;
.preview-btn, .download-btn {
flex: 1;
height: 64rpx;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.preview-btn {
background-color: #409eff;
color: #fff;
&:active {
background-color: #337ecc;
}
}
.download-btn {
background-color: #67c23a;
color: #fff;
&:active {
background-color: #529b2e;
}
}
}
}
/* 加载状态 */

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

@ -0,0 +1,829 @@
<!--员工合同签署表单页面-->
<template>
<view class="main_box">
<!-- 自定义导航栏 -->
<view class="navbar_section">
<view class="navbar_back" @click="goBack">
<text class="back_icon"></text>
</view>
<view class="navbar_title">合同签署</view>
<view class="navbar_action"></view>
</view>
<!-- 合同信息 -->
<view class="contract_info_section" v-if="contractInfo">
<view class="info_header">
<view class="contract_name">{{ contractInfo.contract_name }}</view>
<view class="contract_type">{{ contractInfo.contract_type }}</view>
</view>
</view>
<!-- 合同内容 -->
<view class="contract_content_section" v-if="contractContent">
<view class="content_header">
<view class="content_title">合同内容</view>
<view class="content_expand_btn" @click="toggleContentExpand">
{{ contentExpanded ? '收起' : '展开' }}
</view>
</view>
<view class="content_body" :class="{ expanded: contentExpanded }">
<text class="content_text">{{ renderContractContent }}</text>
</view>
</view>
<!-- 表单内容 -->
<view class="form_section" v-if="!loading">
<view class="form_title">请填写以下信息</view>
<view class="form_content">
<view
v-for="(field, index) in formFields"
:key="index"
class="form_field"
>
<view class="field_label">
{{ field.name }}
<text v-if="field.is_required" class="required_mark">*</text>
</view>
<!-- 手写签名字段 -->
<view v-if="field.data_type === 'signature'" class="field_signature">
<view
class="signature_button"
@click="goToSignature(field)"
:class="formData[field.placeholder] ? 'signed' : ''"
>
<text class="signature_icon"></text>
<text class="signature_text">
{{ formData[field.placeholder] ? '已签名' : '手写签名' }}
</text>
</view>
<view v-if="formData[field.placeholder]" class="signature_preview">
<image :src="formData[field.placeholder]" mode="aspectFit" />
</view>
</view>
<!-- 签名图片上传字段 -->
<view v-else-if="field.data_type === 'sign_img'" class="field_sign_img">
<view
class="sign_img_button"
@click="chooseSignImage(field)"
:class="formData[field.placeholder] ? 'uploaded' : ''"
>
<text class="sign_img_icon">📷</text>
<text class="sign_img_text">
{{ formData[field.placeholder] ? '已上传' : '上传签名图片' }}
</text>
</view>
<view v-if="formData[field.placeholder]" class="sign_img_preview">
<image :src="formData[field.placeholder]" mode="aspectFit" />
</view>
</view>
<!-- 文本输入框 -->
<view v-else-if="field.field_type === 'text'" class="field_input">
<input
type="text"
:placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''"
/>
</view>
<!-- 日期选择器 -->
<view v-else-if="field.field_type === 'date'" class="field_input">
<picker
mode="date"
:value="formData[field.placeholder]"
@change="handleDateChange(field.placeholder, $event)"
:disabled="field.data_type !== 'user_input'"
>
<input
type="text"
:placeholder="field.placeholder || '请选择' + field.name"
:value="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''"
readonly
/>
</picker>
</view>
<!-- 数字输入框 -->
<view v-else-if="field.field_type === 'number'" class="field_input">
<input
type="number"
:placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''"
/>
</view>
<!-- 文本域 -->
<view v-else-if="field.field_type === 'textarea'" class="field_textarea">
<textarea
:placeholder="field.placeholder || '请输入' + field.name"
v-model="formData[field.placeholder]"
:disabled="field.data_type !== 'user_input'"
:class="field.data_type !== 'user_input' ? 'disabled' : ''"
></textarea>
</view>
<!-- 提示信息 -->
<view v-if="field.data_type !== 'user_input' && field.data_type !== 'signature' && field.data_type !== 'sign_img'" class="field_hint">
{{ getFieldHint(field) }}
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading_section">
<view class="loading_spinner"></view>
<view class="loading_text">加载表单配置中...</view>
</view>
<!-- 提交按钮 -->
<view class="submit_section" v-if="!loading">
<button
class="submit_btn"
:disabled="submitting"
@click="submitContract"
>
{{ submitting ? '提交中...' : '提交签署' }}
</button>
</view>
</view>
</template>
<script>
import apiRoute from '@/common/axios.js'
export default {
data() {
return {
contractId: 0,
personnelId: 0,
contractName: '',
contractInfo: null,
contractContent: '',
contentExpanded: false,
formFields: [],
formData: {},
loading: true,
submitting: false
}
},
computed: {
//
renderContractContent() {
if (!this.contractContent) return ''
let content = this.contractContent
//
Object.keys(this.formData).forEach(key => {
const value = this.formData[key] || ''
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g')
content = content.replace(regex, value)
})
return content
}
},
onLoad(options) {
this.contractId = parseInt(options.contract_id) || 0
this.personnelId = parseInt(options.personnel_id) || 0
this.contractName = decodeURIComponent(options.contract_name || '')
if (this.contractId && this.personnelId) {
this.loadSignForm()
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
},
onShow() {
//
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage.data && currentPage.data.newSignature) {
const { fieldPlaceholder, signatureData } = currentPage.data.newSignature
this.formData[fieldPlaceholder] = signatureData
//
delete currentPage.data.newSignature
}
},
methods: {
goBack() {
uni.navigateBack()
},
async loadSignForm() {
this.loading = true
try {
console.log('加载员工签署表单:', { contractId: this.contractId, personnelId: this.personnelId })
//
const response = await apiRoute.get('/contract/signForm', {
contract_id: this.contractId
})
if (response.code === 1) {
const data = response.data
this.contractInfo = {
contract_name: data.contract_name,
contract_type: data.contract_type
}
this.contractContent = data.contract_content || ''
this.formFields = data.form_fields || []
//
this.initFormData()
console.log('表单配置加载成功:', this.formFields)
} else {
uni.showToast({
title: response.msg || '获取表单配置失败',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
} catch (error) {
console.error('获取表单配置失败:', error)
uni.showToast({
title: '获取表单配置失败',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} finally {
this.loading = false
}
},
initFormData() {
const data = {}
this.formFields.forEach(field => {
const key = field.placeholder || field.name
//
if (field.data_type === 'database' || field.data_type === 'system') {
data[key] = field.default_value || ''
} else {
data[key] = field.default_value || ''
}
})
this.formData = data
},
handleDateChange(fieldKey, event) {
this.formData[fieldKey] = event.detail.value
},
toggleContentExpand() {
this.contentExpanded = !this.contentExpanded
},
goToSignature(field) {
//
const fieldPlaceholder = field.placeholder || field.name
uni.navigateTo({
url: `/pages-common/contract/contract_sign?field_name=${encodeURIComponent(field.name)}&field_placeholder=${encodeURIComponent(fieldPlaceholder)}&contract_id=${this.contractId}&isStaff=true`
})
},
chooseSignImage(field) {
//
const fieldPlaceholder = field.placeholder || field.name
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
//
this.uploadSignImage(tempFilePath, fieldPlaceholder)
},
fail: (err) => {
console.error('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none'
})
}
})
},
async uploadSignImage(tempFilePath, fieldPlaceholder) {
//
uni.showLoading({
title: '上传中...',
mask: true
})
try {
//
const uploadResult = await new Promise((resolve, reject) => {
uni.uploadFile({
url: 'http://localhost:20080/api/uploadImage',
filePath: tempFilePath,
name: 'image',
header: {
'token': uni.getStorageSync('token') || ''
},
success: (uploadRes) => {
try {
const result = JSON.parse(uploadRes.data)
if (result.code === 1) {
resolve(result.data)
} else {
reject(new Error(result.msg || '上传失败'))
}
} catch (e) {
reject(new Error('上传响应解析失败'))
}
},
fail: (err) => {
reject(new Error('上传请求失败'))
}
})
})
// 使URL
this.formData[fieldPlaceholder] = uploadResult.url || uploadResult.path || tempFilePath
uni.hideLoading()
uni.showToast({
title: '上传成功',
icon: 'success'
})
console.log('签名图片上传成功:', uploadResult)
} catch (error) {
console.error('签名图片上传失败:', error)
// 使
this.formData[fieldPlaceholder] = tempFilePath
uni.hideLoading()
uni.showToast({
title: '上传失败,使用本地图片',
icon: 'none',
duration: 2000
})
}
},
getFieldHint(field) {
switch (field.data_type) {
case 'database':
return '此信息将从系统数据库自动获取'
case 'system':
return '此信息将由系统自动生成'
default:
return ''
}
},
validateForm() {
//
for (const field of this.formFields) {
if (field.is_required) {
const key = field.placeholder || field.name
const value = this.formData[key]
if (!value || value.toString().trim() === '') {
uni.showToast({
title: `请填写${field.name}`,
icon: 'none'
})
return false
}
}
}
return true
},
async submitContract() {
if (!this.validateForm()) {
return
}
this.submitting = true
try {
console.log('提交员工合同签署:', {
contractId: this.contractId,
personnelId: this.personnelId,
formData: this.formData
})
//
const response = await apiRoute.post('/contract/sign', {
contract_id: this.contractId,
form_data: this.formData,
signature_image: this.getSignatureImage()
})
if (response.code === 1) {
uni.showToast({
title: '合同签署成功',
icon: 'success',
duration: 2000
})
setTimeout(() => {
//
uni.navigateBack()
}, 2000)
} else {
uni.showToast({
title: response.msg || '合同签署失败',
icon: 'none'
})
}
} catch (error) {
console.error('合同签署失败:', error)
uni.showToast({
title: '合同签署失败',
icon: 'none'
})
} finally {
this.submitting = false
}
},
getSignatureImage() {
// signature
for (const field of this.formFields) {
if (field.data_type === 'signature') {
const key = field.placeholder || field.name
return this.formData[key] || ''
}
}
return ''
}
}
}
</script>
<style lang="scss" scoped>
.main_box {
background: #1a1a1a;
min-height: 100vh;
color: #fff;
padding-bottom: 120rpx;
}
//
.navbar_section {
display: flex;
justify-content: space-between;
align-items: center;
background: #29D3B4;
padding: 40rpx 32rpx 20rpx;
//
// #ifdef MP-WEIXIN
padding-top: 80rpx;
// #endif
.navbar_back {
width: 60rpx;
.back_icon {
color: #fff;
font-size: 40rpx;
font-weight: 600;
}
}
.navbar_title {
color: #fff;
font-size: 32rpx;
font-weight: 600;
}
.navbar_action {
width: 60rpx;
}
}
//
.contract_info_section {
background: #2a2a2a;
margin: 20rpx;
border-radius: 16rpx;
padding: 24rpx;
.info_header {
.contract_name {
font-size: 32rpx;
font-weight: 600;
color: #fff;
margin-bottom: 8rpx;
}
.contract_type {
font-size: 24rpx;
color: #ccc;
}
}
}
//
.contract_content_section {
background: #2a2a2a;
margin: 20rpx;
border-radius: 16rpx;
padding: 24rpx;
.content_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.content_title {
font-size: 28rpx;
font-weight: 600;
color: #fff;
}
.content_expand_btn {
font-size: 24rpx;
color: #29D3B4;
padding: 8rpx 16rpx;
border: 1rpx solid #29D3B4;
border-radius: 8rpx;
background: rgba(41, 211, 180, 0.05);
}
}
.content_body {
max-height: 200rpx;
overflow: hidden;
transition: max-height 0.3s ease;
&.expanded {
max-height: none;
}
.content_text {
font-size: 26rpx;
color: #ccc;
line-height: 1.6;
word-break: break-all;
}
}
}
//
.form_section {
margin: 20rpx;
.form_title {
font-size: 28rpx;
font-weight: 600;
color: #fff;
margin-bottom: 20rpx;
padding: 0 8rpx;
}
.form_content {
.form_field {
background: #2a2a2a;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.field_label {
font-size: 28rpx;
color: #fff;
margin-bottom: 16rpx;
font-weight: 500;
.required_mark {
color: #e74c3c;
margin-left: 4rpx;
}
}
.field_input {
input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
border: 2rpx solid #444;
border-radius: 12rpx;
font-size: 28rpx;
color: #fff;
background: #1a1a1a;
&::placeholder {
color: #666;
}
&.disabled {
background: #333;
color: #999;
}
}
}
.field_textarea {
textarea {
width: 100%;
min-height: 120rpx;
padding: 20rpx;
border: 2rpx solid #444;
border-radius: 12rpx;
font-size: 28rpx;
color: #fff;
background: #1a1a1a;
&::placeholder {
color: #666;
}
&.disabled {
background: #333;
color: #999;
}
}
}
.field_signature {
.signature_button {
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
border: 2rpx dashed #29D3B4;
border-radius: 12rpx;
background: rgba(41, 211, 180, 0.05);
&.signed {
border-color: #27ae60;
background: rgba(39, 174, 96, 0.05);
}
.signature_icon {
font-size: 32rpx;
margin-right: 8rpx;
}
.signature_text {
font-size: 28rpx;
color: #29D3B4;
.signed & {
color: #27ae60;
}
}
}
.signature_preview {
margin-top: 16rpx;
image {
width: 100%;
height: 120rpx;
border-radius: 8rpx;
border: 1rpx solid #444;
}
}
}
.field_sign_img {
.sign_img_button {
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
border: 2rpx dashed #409eff;
border-radius: 12rpx;
background: rgba(64, 158, 255, 0.05);
&.uploaded {
border-color: #67c23a;
background: rgba(103, 194, 58, 0.05);
}
.sign_img_icon {
font-size: 32rpx;
margin-right: 8rpx;
}
.sign_img_text {
font-size: 28rpx;
color: #409eff;
.uploaded & {
color: #67c23a;
}
}
}
.sign_img_preview {
margin-top: 16rpx;
image {
width: 100%;
height: 120rpx;
border-radius: 8rpx;
border: 1rpx solid #444;
}
}
}
.field_hint {
margin-top: 8rpx;
font-size: 22rpx;
color: #666;
font-style: italic;
}
}
}
}
//
.loading_section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 32rpx;
.loading_spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #444;
border-left: 4rpx solid #29D3B4;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
.loading_text {
font-size: 28rpx;
color: #ccc;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
//
.submit_section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #2a2a2a;
padding: 20rpx 32rpx 40rpx;
border-top: 1rpx solid #444;
//
// #ifdef MP-WEIXIN
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
// #endif
.submit_btn {
width: 100%;
height: 72rpx;
background-color: #29d3b4;
color: #fff;
border: none;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
&:active {
background-color: #22b39a;
}
&:disabled {
background-color: #666;
color: #999;
}
}
}
</style>

10
uniapp/pages.json

@ -630,6 +630,16 @@
"backgroundColor": "#181A20"
}
},
{
"path": "contract/staff-contract-sign",
"style": {
"navigationBarTitleText": "员工合同签署",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#1a1a1a",
"navigationBarTextStyle": "white",
"backgroundColor": "#1a1a1a"
}
},
{
"path": "profile/personal_info",
"style": {

2
uniapp/pages/common/profile/index.vue

@ -57,7 +57,7 @@
title: '我的合同',
icon: 'compose',
desc: '查看签署合同',
path: '/pages-common/contract/list'
path: '/pages-common/contract/my_contract'
},
{
title: '我的工资',

Loading…
Cancel
Save