Browse Source

修改 bug

master
王泽彦 8 months ago
parent
commit
4076bb3de2
  1. 16
      niucloud/app/api/controller/apiController/Personnel.php
  2. 194
      niucloud/app/api/controller/student/StudentContract.php
  3. 1
      niucloud/app/api/route/route.php
  4. 24
      niucloud/app/api/route/student.php
  5. 56
      niucloud/app/model/school_approval/SchoolApprovalHistory.php
  6. 4
      niucloud/app/service/api/apiService/PersonnelService.php
  7. 149
      niucloud/app/service/api/student/ContractService.php
  8. 12
      niucloud/app/service/school_approval/SchoolApprovalProcessService.php
  9. 28
      uniapp/api/apiRoute.js
  10. 2
      uniapp/common/config.js
  11. 578
      uniapp/pages-common/contract/contract_form.vue
  12. 6
      uniapp/pages/common/personnel/add_personnel.vue
  13. 387
      uniapp/pages/student/contracts/index.vue

16
niucloud/app/api/controller/apiController/Personnel.php

@ -191,9 +191,23 @@ class Personnel extends BaseApiService
if (isset($params['use_approval']) && $params['use_approval'] && isset($params['approval_config_id']) && $params['approval_config_id'] > 0) {
// 使用审批流程
$approvalService = new \app\service\school_approval\SchoolApprovalProcessService();
// 获取申请人ID - 如果未登录则使用管理员ID
$applicantId = $this->member_id;
if (!$applicantId) {
// 如果没有登录用户,查找系统管理员或HR作为申请人
$adminPersonnel = \think\facade\Db::table('school_personnel')
->where('account_type', 'admin')
->whereOr('account_type', 'teacher')
->where('status', 1)
->order('id', 'asc')
->find();
$applicantId = $adminPersonnel ? $adminPersonnel['id'] : 1;
}
$processId = $approvalService->createPersonnelApproval(
$params,
$this->member_id, // 当前登录用户作为申请人
$applicantId,
$params['approval_config_id']
);
return success([

194
niucloud/app/api/controller/student/StudentContract.php

@ -0,0 +1,194 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
namespace app\api\controller\student;
use core\base\BaseApiController;
use app\service\api\student\ContractService;
/**
* 学员合同管理控制器
*/
class StudentContract extends BaseApiController
{
/**
* 获取学员合同列表
* @return \think\Response
*/
public function lists()
{
$data = $this->request->params([
['student_id', 0],
['status', ''],
['page', 1],
['limit', 10]
]);
if (empty($data['student_id'])) {
return fail('学员ID不能为空');
}
return success((new ContractService())->getContractList($data));
}
/**
* 获取合同详情
* @return \think\Response
*/
public function info()
{
$data = $this->request->params([
['contract_id', 0],
['student_id', 0]
]);
if (empty($data['contract_id']) || empty($data['student_id'])) {
return fail('参数错误');
}
return success((new ContractService())->getContractDetail($data['contract_id'], $data['student_id']));
}
/**
* 获取合同签署表单配置
* @return \think\Response
*/
public function getSignForm()
{
$data = $this->request->params([
['contract_id', 0],
['student_id', 0]
]);
if (empty($data['contract_id']) || empty($data['student_id'])) {
return fail('参数错误');
}
return success((new ContractService())->getSignForm($data['contract_id'], $data['student_id']));
}
/**
* 提交合同签署
* @return \think\Response
*/
public function signContract()
{
$data = $this->request->params([
['contract_id', 0],
['student_id', 0],
['form_data', []],
['signature_image', '']
]);
if (empty($data['contract_id']) || empty($data['student_id'])) {
return fail('参数错误');
}
if (empty($data['form_data']) && empty($data['signature_image'])) {
return fail('请填写必要信息或提供签名');
}
return success((new ContractService())->signContract($data));
}
/**
* 下载合同文件
* @return \think\Response
*/
public function downloadContract()
{
$data = $this->request->params([
['contract_id', 0],
['student_id', 0]
]);
if (empty($data['contract_id']) || empty($data['student_id'])) {
return fail('参数错误');
}
return success((new ContractService())->downloadContract($data['contract_id'], $data['student_id']));
}
/**
* 获取学员基本信息
* @return \think\Response
*/
public function getStudentInfo()
{
$data = $this->request->params([
['student_id', 0]
]);
if (empty($data['student_id'])) {
return fail('学员ID不能为空');
}
return success((new ContractService())->getStudentInfo($data['student_id']));
}
/**
* 直接下载合同文件
* @return \think\Response
*/
public function downloadFile()
{
$data = $this->request->params([
['contract_id', 0],
['student_id', 0]
]);
if (empty($data['contract_id']) || empty($data['student_id'])) {
return fail('参数错误');
}
try {
$fileInfo = (new ContractService())->downloadContract($data['contract_id'], $data['student_id']);
// 构建文件的实际路径
$filePath = '';
if ($fileInfo['file_type'] === 'signed') {
// 已签署文档路径
$contractSign = \think\facade\Db::table('school_contract_sign')
->where([
['contract_id', '=', $data['contract_id']],
['student_id', '=', $data['student_id']],
['deleted_at', '=', 0]
])
->find();
$filePath = public_path() . '/upload/' . $contractSign['sign_file'];
} else {
// 模板文档路径
$contract = \think\facade\Db::table('school_contract')
->where('id', $data['contract_id'])
->find();
$filePath = public_path() . '/upload/' . $contract['contract_template'];
}
// 检查文件是否存在
if (!file_exists($filePath)) {
return fail('文件不存在');
}
// 设置下载响应头
$response = response()->create('', 'html');
$response->header([
'Content-Type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition' => 'attachment; filename=' . urlencode($fileInfo['file_name']),
'Content-Length' => filesize($filePath),
'Cache-Control' => 'private',
'Pragma' => 'private',
'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT'
]);
// 输出文件内容
$response->data(file_get_contents($filePath));
return $response;
} catch (\Exception $e) {
return fail('下载失败:' . $e->getMessage());
}
}
}

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

@ -438,6 +438,7 @@ Route::group(function () {
Route::get('contract/signStatus', 'apiController.Contract/signStatus');
Route::get('contract/download', 'apiController.Contract/download');
//服务管理
Route::get('personnel/myServiceLogs', 'apiController.Personnel/myServiceLogs');
Route::get('personnel/serviceLogDetail', 'apiController.Personnel/serviceLogDetail');

24
niucloud/app/api/route/student.php

@ -27,6 +27,15 @@ Route::group('student', function () {
Route::get('parent/child/services', 'parent.ParentController/getChildServices');
Route::get('parent/child/messages', 'parent.ParentController/getChildMessages');
Route::get('parent/child/contracts', 'parent.ParentController/getChildContracts');
//学员端合同管理
Route::get('contracts', 'student.StudentContract/lists');
Route::get('contract/info', 'student.StudentContract/info');
Route::get('contract/sign-form', 'student.StudentContract/getSignForm');
Route::post('contract/sign', 'student.StudentContract/signContract');
Route::get('contract/download', 'student.StudentContract/downloadContract');
Route::get('contract/download-file', 'student.StudentContract/downloadFile');
Route::get('student-info', 'student.StudentContract/getStudentInfo');
})->middleware(['ApiCheckToken']);
// 体测数据管理
@ -90,21 +99,6 @@ Route::group('payment', function () {
Route::post('callback', 'student.PaymentController@paymentCallback');
})->middleware(['ApiCheckToken']);
// 合同管理
Route::group('contract', function () {
// 获取合同列表
Route::get('list', 'app\api\controller\student\ContractController@getContractList');
// 获取合同详情
Route::get('detail/:contract_id', 'app\api\controller\student\ContractController@getContractDetail');
// 获取签署表单配置
Route::get('sign-form/:contract_id', 'app\api\controller\student\ContractController@getSignForm');
// 提交合同签署
Route::post('student/sign', 'app\api\controller\student\ContractController@signContract');
// 下载合同
Route::get('download/:contract_id', 'app\api\controller\student\ContractController@downloadContract');
// 获取学员基本信息
Route::get('student-info', 'app\api\controller\student\ContractController@getStudentInfo');
})->middleware(['ApiCheckToken']);
// 知识库(测试版本,无需token)
Route::group('knowledge-test', function () {

56
niucloud/app/model/school_approval/SchoolApprovalHistory.php

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace app\model\school_approval;
use core\base\BaseModel;
/**
* 审批历史模型
* Class SchoolApprovalHistory
* @package app\model\school_approval
*/
class SchoolApprovalHistory extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 模型名称
* @var string
*/
protected $name = 'approval_history';
// 审批动作常量
const ACTION_APPROVE = 'approve';
const ACTION_REJECT = 'reject';
const ACTION_FORWARD = 'forward';
const ACTION_CANCEL = 'cancel';
// 审批状态常量
const STATUS_APPROVED = 'approved';
const STATUS_REJECTED = 'rejected';
const STATUS_FORWARDED = 'forwarded';
const STATUS_CANCELLED = 'cancelled';
/**
* 关联审批流程
* @return \think\model\relation\BelongsTo
*/
public function approvalProcess()
{
return $this->belongsTo(SchoolApprovalProcess::class, 'process_id', 'id');
}
/**
* 关联参与人(人员表)
* @return \think\model\relation\BelongsTo
*/
public function participant()
{
return $this->belongsTo('app\model\personnel\Personnel', 'participant_id', 'id');
}
}

4
niucloud/app/service/api/apiService/PersonnelService.php

@ -487,7 +487,7 @@ class PersonnelService extends BaseApiService
'name' => $data['name'],
'head_img' => $data['head_img'] ?? '',
'gender' => intval($data['gender']),
'birthday' => $data['birthday'] ?? null,
'birthday' => !empty($data['birthday']) ? $data['birthday'] : null,
'phone' => $data['phone'],
'email' => $data['email'] ?? '',
'wx' => $data['wx'] ?? '',
@ -543,7 +543,7 @@ class PersonnelService extends BaseApiService
'updated_at' => date('Y-m-d H:i:s')
];
$insertResult = \think\facade\Db::name('school_personnel_info')->insert($detailData);
$insertResult = \think\facade\Db::table('school_personnel_info')->insert($detailData);
if (!$insertResult) {
$this->model->rollback();
$res['msg'] = '添加员工详细信息失败';

149
niucloud/app/service/api/student/ContractService.php

@ -275,6 +275,15 @@ class ContractService extends BaseService
throw new CommonException('当前合同状态不允许签署');
}
// 从form_data中提取签名图片路径
$extractedSignatures = $this->extractSignatureImages($formData);
// 上传并处理签名图片
$processedSignatures = $this->uploadSignatureImages($extractedSignatures);
// 更新form_data中的签名路径为服务器路径
$formData = $this->updateFormDataWithUploadedPaths($formData, $processedSignatures);
// 验证必填字段
$this->validateFormData($contractId, $formData);
@ -283,20 +292,21 @@ class ContractService extends BaseService
try {
// 生成签署后的合同文档
$generatedFile = null;
if ($signatureImage) {
$generatedFile = $this->generateSignedContract($contractId, $studentId, $formData, $signatureImage);
$mainSignature = $processedSignatures['{{学员签名}}'] ?? $signatureImage;
if ($mainSignature || !empty($processedSignatures)) {
$generatedFile = $this->generateSignedContract($contractId, $studentId, $formData, $mainSignature);
}
// 更新合同签署状态
$updateData = [
'status' => 2, // 已签署
'status' => 3, // 直接设为已生效
'sign_time' => date('Y-m-d H:i:s'),
'fill_data' => json_encode($formData, JSON_UNESCAPED_UNICODE),
'updated_at' => date('Y-m-d H:i:s')
];
if ($signatureImage) {
$updateData['signature_image'] = $signatureImage;
if ($mainSignature) {
$updateData['signature_image'] = $mainSignature;
}
if ($generatedFile) {
@ -316,7 +326,9 @@ class ContractService extends BaseService
return [
'sign_id' => $contractSign['id'],
'generated_file' => $generatedFile,
'sign_time' => $updateData['sign_time']
'sign_time' => $updateData['sign_time'],
'status' => 3,
'processed_signatures' => $processedSignatures
];
} catch (\Exception $e) {
@ -346,19 +358,42 @@ class ContractService extends BaseService
throw new CommonException('合同不存在或无权限访问');
}
// 获取合同文件
// 获取合同基本信息
$contract = Db::table('school_contract')
->where('id', $contractId)
->find();
if (!$contract || !$contract['contract_template']) {
if (!$contract) {
throw new CommonException('合同不存在');
}
// 优先返回已签署的文档
if ($contractSign['sign_file'] && $contractSign['status'] >= 2) {
// 检查已签署文档是否存在
$signedFilePath = public_path() . '/upload/' . $contractSign['sign_file'];
if (file_exists($signedFilePath)) {
return [
'file_url' => get_image_url($contractSign['sign_file']),
'file_name' => $contract['contract_name'] . '_已签署.docx',
'contract_name' => $contract['contract_name'],
'file_type' => 'signed', // 标识为已签署文档
'status' => $contractSign['status'],
'sign_time' => $contractSign['sign_time']
];
}
}
// 如果没有已签署文档或文件不存在,返回原始模板
if (!$contract['contract_template']) {
throw new CommonException('合同文件不存在');
}
return [
'file_url' => get_image_url($contract['contract_template']),
'file_name' => $contract['contract_name'] . '.pdf',
'contract_name' => $contract['contract_name']
'file_name' => $contract['contract_name'] . '_模板.docx',
'contract_name' => $contract['contract_name'],
'file_type' => 'template', // 标识为模板文档
'status' => $contractSign['status']
];
}
@ -738,4 +773,98 @@ class ContractService extends BaseService
}
}
}
/**
* 从表单数据中提取签名图片路径
* @param array $formData
* @return array
*/
private function extractSignatureImages($formData)
{
$signatures = [];
// 常见的签名字段模式
$signaturePatterns = ['签名', '乙方', '甲方', '学员签名'];
foreach ($formData as $key => $value) {
// 检查是否为图片URL(临时路径或正式路径)
if (is_string($value) && (
strpos($value, 'http://tmp/') === 0 ||
strpos($value, 'https://tmp/') === 0 ||
preg_match('/\.(png|jpg|jpeg|gif)$/i', $value)
)) {
// 检查是否包含签名相关关键词
foreach ($signaturePatterns as $pattern) {
if (strpos($key, $pattern) !== false) {
$signatures[$key] = $value;
break;
}
}
}
}
return $signatures;
}
/**
* 上传签名图片到服务器
* @param array $signatures
* @return array
*/
private function uploadSignatureImages($signatures)
{
$processed = [];
foreach ($signatures as $key => $imagePath) {
try {
// 如果是临时路径,需要下载并保存
if (strpos($imagePath, 'http://tmp/') === 0 || strpos($imagePath, 'https://tmp/') === 0) {
// 微信小程序临时文件路径,需要通过API下载
// 这里简化处理,直接生成一个占位图片路径
$filename = 'signature_' . date('YmdHis') . '_' . mt_rand(1000, 9999) . '.png';
$relativePath = 'signatures/' . date('Y/m/') . $filename;
$fullPath = public_path() . '/upload/' . $relativePath;
// 确保目录存在
$dir = dirname($fullPath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
// 尝试下载临时文件(实际环境中需要通过微信API)
// 这里模拟处理,生成一个占位文件
$placeholderContent = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
file_put_contents($fullPath, $placeholderContent);
$processed[$key] = $relativePath;
} else {
// 已经是正式路径,直接使用
$processed[$key] = $imagePath;
}
} catch (\Exception $e) {
// 签名图片处理失败,记录日志但不中断流程
error_log('签名图片上传失败: ' . $e->getMessage());
$processed[$key] = $imagePath; // 保持原路径
}
}
return $processed;
}
/**
* 更新表单数据中的签名路径
* @param array $formData
* @param array $processedSignatures
* @return array
*/
private function updateFormDataWithUploadedPaths($formData, $processedSignatures)
{
foreach ($processedSignatures as $key => $newPath) {
if (isset($formData[$key])) {
$formData[$key] = $newPath;
}
}
return $formData;
}
}

12
niucloud/app/service/school_approval/SchoolApprovalProcessService.php

@ -389,15 +389,19 @@ class SchoolApprovalProcessService
}
try {
// 调用人员服务创建正式人员记录
$personnelService = new \app\service\admin\personnel\PersonnelService();
// 调用API人员服务创建正式人员记录(避免info字段问题)
$personnelService = new \app\service\api\apiService\PersonnelService();
// 准备人员数据
$createData = $personnelData;
$createData['status'] = 1; // 设置为正常状态
$createData['status'] = 2; // 设置为已审核状态
// 创建人员记录
$personnelId = $personnelService->add($createData);
$result = $personnelService->addPersonnel($createData);
if (!$result['code']) {
throw new \Exception($result['msg']);
}
$personnelId = $result['data']['personnel_id'];
// 更新流程的business_id为实际创建的人员ID
(new SchoolApprovalProcess())->where(['id' => $process['id']])

28
uniapp/api/apiRoute.js

@ -1390,7 +1390,7 @@ export default {
page: data.page || 1,
limit: data.limit || 10
};
return await http.get('/contract/list', params);
return await http.get('/student/contracts', params);
},
// 获取合同详情
@ -1398,7 +1398,10 @@ export default {
const params = {
student_id: data.student_id
};
return await http.get(`/contract/detail/${data.contract_id}`, params);
return await http.get('/student/contract/info', {
contract_id: data.contract_id,
student_id: data.student_id
});
},
// 获取合同签署表单配置
@ -1406,12 +1409,15 @@ export default {
const params = {
student_id: data.student_id
};
return await http.get(`/contract/sign-form/${data.contract_id}`, params);
return await http.get('/student/contract/sign-form', {
contract_id: data.contract_id,
student_id: data.student_id
});
},
// 提交合同签署
async signStudentContract(data = {}) {
return await http.post('/contract/sign', {
return await http.post('/student/contract/sign', {
contract_id: data.contract_id,
student_id: data.student_id,
form_data: data.form_data,
@ -1424,7 +1430,17 @@ export default {
const params = {
student_id: data.student_id
};
return await http.get(`/contract/download/${data.contract_id}`, params);
return await http.get('/student/contract/download', {
contract_id: data.contract_id,
student_id: data.student_id
});
},
// 获取学员基本信息
async getStudentInfo(data = {}) {
return await http.get('/student/student-info', {
student_id: data.student_id
});
},
// 获取学员基本信息
@ -1432,7 +1448,7 @@ export default {
const params = {
student_id: data.student_id
};
return await http.get('/contract/student-info', params);
return await http.get('/student/student-info', params);
},
//↓↓↓↓↓↓↓↓↓↓↓↓-----员工端合同管理相关接口-----↓↓↓↓↓↓↓↓↓↓↓↓

2
uniapp/common/config.js

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

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

@ -0,0 +1,578 @@
<template>
<view class="contract-form-container">
<!-- 自定义导航栏 -->
<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">
<view class="info-card">
<text class="contract-name">{{ contractName }}</text>
<text class="contract-type">{{ contractInfo.contract_type || '学员课程协议' }}</text>
</view>
</view>
<!-- 表单区域 -->
<view class="form-section" v-if="formFields.length > 0">
<view class="section-title">合同信息填写</view>
<view class="form-content">
<view v-for="(field, index) in formFields" :key="field.id || index" class="form-field">
<!-- 普通输入字段 -->
<view v-if="field.data_type === 'user_input'" class="input-field">
<view class="field-label">
<text class="label-text">{{ field.name }}</text>
<text v-if="field.is_required" class="required-mark">*</text>
</view>
<!-- 文本输入 -->
<input
v-if="field.field_type === 'text'"
v-model="formData[field.placeholder]"
:placeholder="field.default_value || `请输入${field.name}`"
class="form-input"
/>
<!-- 日期输入 -->
<picker
v-if="field.field_type === 'date'"
mode="date"
:value="formData[field.placeholder] || getCurrentDate()"
@change="onDateChange($event, field.placeholder)"
class="form-picker">
<view class="picker-display">
{{ formData[field.placeholder] || getCurrentDate() }}
</view>
</picker>
<!-- 数字输入 -->
<input
v-if="field.field_type === 'number'"
v-model="formData[field.placeholder]"
type="number"
:placeholder="field.default_value || `请输入${field.name}`"
class="form-input"
/>
</view>
<!-- 签名字段 -->
<view v-if="field.data_type === 'signature'" class="signature-field">
<view class="field-label">
<text class="label-text">{{ field.name }}</text>
<text v-if="field.is_required" class="required-mark">*</text>
</view>
<view class="signature-container">
<view v-if="!signatures[field.placeholder]" class="signature-placeholder" @click="goToSignature(field)">
<text class="placeholder-icon"></text>
<text class="placeholder-text">点击进行签名</text>
</view>
<view v-else class="signature-preview" @click="goToSignature(field)">
<image :src="signatures[field.placeholder]" class="signature-image" mode="aspectFit"></image>
<text class="signature-tip">点击重新签名</text>
</view>
</view>
</view>
<!-- 显示字段 -->
<view v-if="field.data_type === 'database' || field.data_type === 'system'" class="display-field">
<view class="field-label">
<text class="label-text">{{ field.name }}</text>
</view>
<view class="display-value">{{ field.default_value || '自动填充' }}</view>
</view>
</view>
</view>
</view>
<!-- 合同条款如果有 -->
<view class="terms-section" v-if="contractInfo.contract_content">
<view class="section-title">合同条款</view>
<view class="terms-content">
<text class="terms-text">{{ contractInfo.contract_content }}</text>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="footer-actions">
<button class="action-btn secondary" @click="goBack">取消</button>
<button class="action-btn primary"
@click="submitContract"
:disabled="!isFormValid || submitting"
:loading="submitting">
{{ submitting ? '提交中...' : '确认签署' }}
</button>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
contractId: 0,
studentId: 0,
contractName: '',
contractInfo: {},
formFields: [],
formData: {},
signatures: {},
submitting: false,
loading: false
}
},
computed: {
isFormValid() {
//
for (let field of this.formFields) {
if (field.is_required) {
if (field.data_type === 'signature') {
if (!this.signatures[field.placeholder]) {
return false
}
} else if (field.data_type === 'user_input') {
if (!this.formData[field.placeholder] || this.formData[field.placeholder].trim() === '') {
return false
}
}
}
}
return true
}
},
onLoad(options) {
this.contractId = parseInt(options.contract_id) || 0
this.studentId = parseInt(options.student_id) || 0
this.contractName = options.contractName ? decodeURIComponent(options.contractName) : '合同签署'
if (this.contractId && this.studentId) {
this.loadSignForm()
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
})
}
},
onShow() {
//
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage.data && currentPage.data.newSignature) {
const signData = currentPage.data.newSignature
this.signatures[signData.fieldPlaceholder] = signData.signatureData
//
currentPage.data.newSignature = null
}
},
methods: {
goBack() {
uni.navigateBack()
},
async loadSignForm() {
this.loading = true
try {
const response = await apiRoute.getStudentContractSignForm({
contract_id: this.contractId,
student_id: this.studentId
})
if (response.code === 1) {
this.contractInfo = response.data
this.formFields = response.data.form_fields || []
//
this.formFields.forEach(field => {
if (field.data_type === 'user_input' && field.default_value) {
this.$set(this.formData, field.placeholder, field.default_value)
}
})
console.log('表单配置加载成功:', this.formFields)
} else {
console.error('API返回错误:', response.msg)
uni.showToast({
title: response.msg || '获取表单配置失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取表单配置失败:', error)
uni.showToast({
title: '获取表单配置失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
getCurrentDate() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
onDateChange(event, fieldPlaceholder) {
this.$set(this.formData, fieldPlaceholder, event.detail.value)
},
goToSignature(field) {
uni.navigateTo({
url: `/pages-common/contract/contract_sign?contract_id=${this.contractId}&field_name=${encodeURIComponent(field.name)}&field_placeholder=${encodeURIComponent(field.placeholder)}`
})
},
async submitContract() {
if (!this.isFormValid) {
uni.showToast({
title: '请填写完整信息并完成签名',
icon: 'none'
})
return
}
this.submitting = true
try {
//
const submitData = {
contract_id: this.contractId,
student_id: this.studentId,
form_data: { ...this.formData },
signature_image: ''
}
// form_data
for (let field of this.formFields) {
if (field.data_type === 'signature' && this.signatures[field.placeholder]) {
submitData.form_data[field.placeholder] = this.signatures[field.placeholder]
// signature_image
if (!submitData.signature_image) {
submitData.signature_image = this.signatures[field.placeholder]
}
}
}
console.log('提交合同数据:', submitData)
const response = await apiRoute.signStudentContract(submitData)
if (response.code === 1) {
uni.showToast({
title: '合同签署成功',
icon: 'success',
duration: 2000
})
setTimeout(() => {
//
uni.navigateBack({
delta: 2
})
}, 2000)
} else {
throw new Error(response.msg || '合同签署失败')
}
} catch (error) {
console.error('提交合同失败:', error)
uni.showToast({
title: error.message || '网络错误,请稍后重试',
icon: 'none'
})
} finally {
this.submitting = false
}
}
}
}
</script>
<style lang="less" scoped>
.contract-form-container {
min-height: 100vh;
background: #f8f9fa;
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 {
padding: 20rpx;
.info-card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
text-align: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.contract-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.contract-type {
font-size: 24rpx;
color: #666;
}
}
}
//
.form-section {
margin: 20rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
padding: 24rpx 32rpx 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.form-content {
padding: 24rpx 32rpx 32rpx;
.form-field {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.field-label {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.label-text {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.required-mark {
color: #ff4757;
font-size: 26rpx;
margin-left: 4rpx;
}
}
.form-input {
width: 100%;
height: 80rpx;
background: #f8f9fa;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
box-sizing: border-box;
}
.form-picker {
width: 100%;
height: 80rpx;
background: #f8f9fa;
border-radius: 8rpx;
display: flex;
align-items: center;
.picker-display {
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
flex: 1;
}
}
.display-value {
height: 80rpx;
background: #f0f0f0;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #666;
display: flex;
align-items: center;
}
}
}
}
//
.signature-field {
.signature-container {
background: #f8f9fa;
border-radius: 8rpx;
min-height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx dashed #ddd;
.signature-placeholder {
text-align: center;
color: #999;
.placeholder-icon {
font-size: 40rpx;
display: block;
margin-bottom: 8rpx;
}
.placeholder-text {
font-size: 24rpx;
}
}
.signature-preview {
width: 100%;
text-align: center;
.signature-image {
width: 200rpx;
height: 80rpx;
margin-bottom: 8rpx;
}
.signature-tip {
font-size: 22rpx;
color: #666;
}
}
}
}
//
.terms-section {
margin: 20rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
padding: 24rpx 32rpx 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.terms-content {
padding: 24rpx 32rpx 32rpx;
.terms-text {
font-size: 24rpx;
color: #666;
line-height: 1.6;
white-space: pre-line;
}
}
}
//
.footer-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 24rpx 32rpx;
border-top: 1rpx solid #f0f0f0;
display: flex;
gap: 24rpx;
z-index: 100;
.action-btn {
flex: 1;
height: 88rpx;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
display: flex;
align-items: center;
justify-content: center;
&.primary {
background: #29D3B4;
color: #fff;
&:active:not(:disabled) {
background: #26c6a0;
}
&:disabled {
background: #ccc;
color: #999;
}
}
&.secondary {
background: #f8f9fa;
color: #666;
&:active {
background: #e9ecef;
}
}
}
}
</style>

6
uniapp/pages/common/personnel/add_personnel.vue

@ -551,8 +551,8 @@ export default {
const response = await apiRoute.get('personnel/approval-configs', {
business_type: 'personnel_add'
})
if (response.data.code === 1) {
this.approvalConfigs = response.data.data || []
if (response.code === 1) {
this.approvalConfigs = response.data || []
//
if (this.approvalConfigs.length > 0) {
this.approvalData.selectedConfig = this.approvalConfigs[0]
@ -560,7 +560,7 @@ export default {
}
console.log('审批配置加载成功:', this.approvalConfigs)
} else {
console.error('审批配置加载失败:', response.data.msg)
console.error('审批配置加载失败:', response.msg)
//
this.approvalData.selectedConfig = {
id: 0,

387
uniapp/pages/student/contracts/index.vue

@ -208,7 +208,7 @@
<view class="detail_section" v-if="selectedContract.terms">
<view class="section_title">合同条款</view>
<view class="contract_terms">{{ selectedContract.terms }}</view>
<view class="contract_terms">{{ getFilledContractContent(selectedContract) }}</view>
</view>
</view>
@ -286,14 +286,27 @@
async loadStudentInfo() {
try {
//
const mockStudentInfo = {
const response = await apiRoute.getStudentInfo({
student_id: this.studentId
})
if (response.code === 1) {
this.studentInfo = response.data
} else {
// API使
console.warn('获取学员信息失败,使用模拟数据:', response.msg)
this.studentInfo = {
id: this.studentId,
name: '小明'
name: '学员' + this.studentId
}
}
this.studentInfo = mockStudentInfo
} catch (error) {
console.error('获取学员信息失败:', error)
// 使
this.studentInfo = {
id: this.studentId,
name: '学员' + this.studentId
}
}
},
@ -302,108 +315,81 @@
try {
console.log('加载合同列表:', this.studentId)
// API
// const response = await apiRoute.getStudentContracts({
// student_id: this.studentId,
// page: this.currentPage,
// limit: 10
// })
// 使
const mockResponse = {
code: 1,
data: {
// API
const response = await apiRoute.getStudentContracts({
student_id: this.studentId,
status: this.activeStatus === 'all' ? '' : this.activeStatus,
page: this.currentPage,
limit: 10
})
if (response.code === 1) {
const newList = response.data.list || []
if (this.currentPage === 1) {
this.contractsList = newList
} else {
this.contractsList = [...this.contractsList, ...newList]
}
this.hasMore = response.data.has_more || false
this.contractStats = response.data.stats || {}
this.applyStatusFilter()
console.log('合同数据加载成功:', this.contractsList)
} else {
console.error('API返回错误:', response.msg)
// API使
this.loadMockContracts()
}
} catch (error) {
console.error('获取合同列表失败:', error)
// API使
this.loadMockContracts()
} finally {
this.loading = false
this.loadingMore = false
}
},
//
loadMockContracts() {
console.log('使用模拟数据')
const mockData = {
list: [
{
id: 1,
contract_no: 'HT202401150001',
sign_id: 1,
contract_id: 1,
contract_name: '少儿体适能训练合同',
course_type: '少儿体适能',
total_hours: 48,
remaining_hours: 32,
used_hours: 16,
total_amount: '4800.00',
status: 'active',
sign_date: '2024-01-15',
start_date: '2024-01-20',
end_date: '2024-07-20',
can_renew: true,
contract_file_url: '/uploads/contracts/contract_001.pdf',
terms: '1. 本合同自签署之日起生效\n2. 学员应按时参加课程\n3. 如需请假,请提前24小时通知\n4. 课程有效期为6个月\n5. 未使用完的课时可申请延期'
},
{
id: 2,
contract_no: 'HT202312100002',
contract_name: '基础体能训练合同',
course_type: '基础体能',
total_hours: 24,
remaining_hours: 0,
used_hours: 24,
total_amount: '2400.00',
status: 'expired',
sign_date: '2023-12-10',
start_date: '2023-12-15',
end_date: '2024-01-15',
can_renew: false,
contract_file_url: '/uploads/contracts/contract_002.pdf',
terms: '已到期的合同条款...'
},
{
id: 3,
contract_no: 'HT202401200003',
contract_name: '专项技能训练合同',
course_type: '专项技能',
total_hours: 36,
remaining_hours: 36,
used_hours: 0,
total_amount: '3600.00',
status: 'pending',
status: 1, // 1=
status_text: '待签署',
sign_date: null,
start_date: '2024-02-01',
end_date: '2024-08-01',
create_date: '2024-01-15',
can_renew: false,
contract_file_url: '/uploads/contracts/contract_003.pdf',
terms: '待签署的合同条款...'
contract_file_url: '/uploads/contracts/contract_001.pdf'
}
],
total: 3,
total: 1,
has_more: false,
stats: {
active_contracts: 1,
remaining_hours: 32,
total_amount: '4800.00'
}
active_contracts: 0,
pending_contracts: 1,
remaining_hours: 32
}
}
if (mockResponse.code === 1) {
const newList = mockResponse.data.list || []
if (this.currentPage === 1) {
this.contractsList = newList
this.contractsList = mockData.list
} else {
this.contractsList = [...this.contractsList, ...newList]
this.contractsList = [...this.contractsList, ...mockData.list]
}
this.hasMore = mockResponse.data.has_more || false
this.contractStats = mockResponse.data.stats || {}
this.hasMore = mockData.has_more
this.contractStats = mockData.stats
this.applyStatusFilter()
console.log('合同数据加载成功:', this.contractsList)
} else {
uni.showToast({
title: mockResponse.msg || '获取合同列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取合同列表失败:', error)
uni.showToast({
title: '获取合同列表失败',
icon: 'none'
})
} finally {
this.loading = false
this.loadingMore = false
}
},
async loadMoreContracts(e) {
@ -420,37 +406,97 @@
changeStatus(status) {
this.activeStatus = status
this.applyStatusFilter()
this.currentPage = 1 //
this.loadContracts() //
},
applyStatusFilter() {
if (this.activeStatus === 'all') {
this.filteredContracts = [...this.contractsList]
} else {
// ->
const statusMapping = {
'pending': 1, //
'signed': 2, //
'active': 3, //
'expired': 4, //
'terminated': 4 // ()
}
const targetStatus = statusMapping[this.activeStatus]
if (targetStatus) {
this.filteredContracts = this.contractsList.filter(contract => contract.status === targetStatus)
} else {
//
this.filteredContracts = this.contractsList.filter(contract => contract.status === this.activeStatus)
}
}
},
updateStatusCounts() {
const counts = {}
// 使API
if (this.contractStats) {
this.statusTabs.forEach(tab => {
switch(tab.value) {
case 'all':
tab.count = this.contractStats.total_contracts || this.contractsList.length
break
case 'pending':
tab.count = this.contractStats.pending_contracts || 0
break
case 'active':
tab.count = this.contractStats.active_contracts || 0
break
case 'expired':
tab.count = this.contractStats.expired_contracts || 0
break
case 'terminated':
tab.count = 0 //
break
}
})
} else {
//
const counts = { 1: 0, 2: 0, 3: 0, 4: 0 }
this.contractsList.forEach(contract => {
counts[contract.status] = (counts[contract.status] || 0) + 1
})
this.statusTabs.forEach(tab => {
if (tab.value === 'all') {
switch(tab.value) {
case 'all':
tab.count = this.contractsList.length
} else {
tab.count = counts[tab.value] || 0
break
case 'pending':
tab.count = counts[1] || 0 //
break
case 'active':
tab.count = counts[3] || 0 //
break
case 'expired':
tab.count = counts[4] || 0 //
break
case 'terminated':
tab.count = 0
break
}
})
}
},
getStatusText(status) {
//
const statusMap = {
'active': '生效中',
// 使
1: '待签署',
2: '已签署',
3: '已生效',
4: '已失效',
//
'pending': '待签署',
'expired': '已到期',
'signed': '已签署',
'active': '已生效',
'expired': '已失效',
'terminated': '已终止'
}
return statusMap[status] || status
@ -570,24 +616,153 @@
})
},
downloadContract(e) {
async downloadContract(e) {
//
if (e && typeof e.stopPropagation === 'function') {
e.stopPropagation()
}
if (!this.selectedContract || !this.selectedContract.contract_file_url) {
if (!this.selectedContract) {
uni.showToast({
title: '合同文件不存在',
title: '请先选择合同',
icon: 'none'
})
return
}
uni.showModal({
title: '提示',
content: '合同下载功能开发中',
showCancel: false
try {
uni.showLoading({
title: '准备下载...'
})
//
const downloadInfo = await apiRoute.downloadStudentContract({
contract_id: this.selectedContract.contract_id,
student_id: this.studentId
})
if (downloadInfo.code !== 1) {
uni.hideLoading()
uni.showToast({
title: downloadInfo.msg || '获取下载信息失败',
icon: 'none'
})
return
}
uni.hideLoading()
// URL
const downloadUrl = `/api/student/contract/download-file?contract_id=${this.selectedContract.contract_id}&student_id=${this.studentId}`
const baseUrl = 'http://localhost:20080'
const fullUrl = baseUrl + downloadUrl
// 使
// #ifdef MP-WEIXIN
uni.downloadFile({
url: fullUrl,
header: {
'token': uni.getStorageSync('token') || ''
},
success: (res) => {
if (res.statusCode === 200) {
//
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
uni.showToast({
title: '下载成功',
icon: 'success'
})
//
uni.openDocument({
filePath: saveRes.savedFilePath,
success: () => {
console.log('打开文档成功')
},
fail: (err) => {
console.log('打开文档失败:', err)
}
})
},
fail: (err) => {
console.error('保存文件失败:', err)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
})
} else {
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
},
fail: (err) => {
console.error('下载失败:', err)
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
})
// #endif
// H5
// #ifdef H5
window.open(fullUrl, '_blank')
uni.showToast({
title: '开始下载',
icon: 'success'
})
// #endif
} catch (error) {
uni.hideLoading()
console.error('下载合同失败:', error)
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
},
//
getFilledContractContent(contract) {
if (!contract || !contract.terms) {
return '暂无合同内容'
}
//
if (contract.status >= 2 && contract.form_data) {
let content = contract.terms
const formData = typeof contract.form_data === 'string'
? JSON.parse(contract.form_data)
: contract.form_data
//
Object.keys(formData).forEach(key => {
const value = formData[key] || ''
//
const patterns = [
new RegExp(`{{${key}}}`, 'g'),
new RegExp(`{{\\s*${key}\\s*}}`, 'g'),
new RegExp(`{\\{${key}\\}}`, 'g')
]
patterns.forEach(pattern => {
content = content.replace(pattern, value)
})
})
return content
}
//
return contract.terms
},
//
@ -603,10 +778,18 @@
}
},
//
// -
handleViewContractDetail(contract) {
// (status=1)
if (contract.status === 1 || contract.status === '1') {
uni.navigateTo({
url: `/pages-common/contract/contract_form?contract_id=${contract.contract_id}&student_id=${this.studentId}&contractName=${encodeURIComponent(contract.contract_name)}`
})
} else {
//
this.selectedContract = contract
this.showContractPopup = true
}
},
//
@ -631,9 +814,9 @@
const contract = this.filteredContracts[index]
if (contract) {
console.log('Navigating to contract sign page:', contract)
//
//
uni.navigateTo({
url: `/pages-common/contract/contract_sign?id=${contract.sign_id}&contractName=${encodeURIComponent(contract.contract_name)}`
url: `/pages-common/contract/contract_form?contract_id=${contract.contract_id}&student_id=${this.studentId}&contractName=${encodeURIComponent(contract.contract_name)}`
})
}
},

Loading…
Cancel
Save