diff --git a/niucloud/app/api/controller/apiController/Personnel.php b/niucloud/app/api/controller/apiController/Personnel.php index ce97a921..23fc0982 100644 --- a/niucloud/app/api/controller/apiController/Personnel.php +++ b/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([ diff --git a/niucloud/app/api/controller/student/StudentContract.php b/niucloud/app/api/controller/student/StudentContract.php new file mode 100644 index 00000000..66520a37 --- /dev/null +++ b/niucloud/app/api/controller/student/StudentContract.php @@ -0,0 +1,194 @@ +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()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/api/route/route.php b/niucloud/app/api/route/route.php index b8855a47..11cd2eec 100644 --- a/niucloud/app/api/route/route.php +++ b/niucloud/app/api/route/route.php @@ -437,6 +437,7 @@ Route::group(function () { Route::post('contract/sign', 'apiController.Contract/sign'); Route::get('contract/signStatus', 'apiController.Contract/signStatus'); Route::get('contract/download', 'apiController.Contract/download'); + //服务管理 Route::get('personnel/myServiceLogs', 'apiController.Personnel/myServiceLogs'); diff --git a/niucloud/app/api/route/student.php b/niucloud/app/api/route/student.php index 79f6610d..2c99bce2 100644 --- a/niucloud/app/api/route/student.php +++ b/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 () { diff --git a/niucloud/app/model/school_approval/SchoolApprovalHistory.php b/niucloud/app/model/school_approval/SchoolApprovalHistory.php new file mode 100644 index 00000000..ad2c752d --- /dev/null +++ b/niucloud/app/model/school_approval/SchoolApprovalHistory.php @@ -0,0 +1,56 @@ +belongsTo(SchoolApprovalProcess::class, 'process_id', 'id'); + } + + /** + * 关联参与人(人员表) + * @return \think\model\relation\BelongsTo + */ + public function participant() + { + return $this->belongsTo('app\model\personnel\Personnel', 'participant_id', 'id'); + } +} \ No newline at end of file diff --git a/niucloud/app/service/api/apiService/PersonnelService.php b/niucloud/app/service/api/apiService/PersonnelService.php index 99189102..43e8c9a8 100644 --- a/niucloud/app/service/api/apiService/PersonnelService.php +++ b/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'] = '添加员工详细信息失败'; diff --git a/niucloud/app/service/api/student/ContractService.php b/niucloud/app/service/api/student/ContractService.php index 52af3f6a..ace2b68b 100644 --- a/niucloud/app/service/api/student/ContractService.php +++ b/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; + } } \ No newline at end of file diff --git a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php b/niucloud/app/service/school_approval/SchoolApprovalProcessService.php index 3ee23064..7b580475 100644 --- a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php +++ b/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']]) diff --git a/uniapp/api/apiRoute.js b/uniapp/api/apiRoute.js index acc2031a..4f8b5194 100644 --- a/uniapp/api/apiRoute.js +++ b/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); }, //↓↓↓↓↓↓↓↓↓↓↓↓-----员工端合同管理相关接口-----↓↓↓↓↓↓↓↓↓↓↓↓ diff --git a/uniapp/common/config.js b/uniapp/common/config.js index 6dfd5008..c98a2253 100644 --- a/uniapp/common/config.js +++ b/uniapp/common/config.js @@ -1,5 +1,5 @@ // 环境变量配置 -const env = 'prod' +const env = 'development' // const env = 'prod' const isMockEnabled = false // 默认禁用Mock优先模式,仅作为回退 const isDebug = false // 默认启用调试模式 diff --git a/uniapp/pages-common/contract/contract_form.vue b/uniapp/pages-common/contract/contract_form.vue new file mode 100644 index 00000000..5b00e656 --- /dev/null +++ b/uniapp/pages-common/contract/contract_form.vue @@ -0,0 +1,578 @@ + + + + + \ No newline at end of file diff --git a/uniapp/pages/common/personnel/add_personnel.vue b/uniapp/pages/common/personnel/add_personnel.vue index dd4585ea..49e90ff9 100644 --- a/uniapp/pages/common/personnel/add_personnel.vue +++ b/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, diff --git a/uniapp/pages/student/contracts/index.vue b/uniapp/pages/student/contracts/index.vue index 3b0c1db9..cda891e0 100644 --- a/uniapp/pages/student/contracts/index.vue +++ b/uniapp/pages/student/contracts/index.vue @@ -208,7 +208,7 @@ 合同条款 - {{ selectedContract.terms }} + {{ getFilledContractContent(selectedContract) }} @@ -286,14 +286,27 @@ async loadStudentInfo() { try { - // 模拟获取学员信息 - const mockStudentInfo = { - id: this.studentId, - name: '小明' + 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: '学员' + this.studentId + } } - this.studentInfo = mockStudentInfo } catch (error) { console.error('获取学员信息失败:', error) + // 使用模拟数据作为备用 + this.studentInfo = { + id: this.studentId, + name: '学员' + this.studentId + } } }, @@ -302,110 +315,83 @@ 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: { - list: [ - { - id: 1, - contract_no: 'HT202401150001', - 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', - sign_date: null, - start_date: '2024-02-01', - end_date: '2024-08-01', - can_renew: false, - contract_file_url: '/uploads/contracts/contract_003.pdf', - terms: '待签署的合同条款...' - } - ], - total: 3, - has_more: false, - stats: { - active_contracts: 1, - remaining_hours: 32, - total_amount: '4800.00' - } - } - } + // 调用真实API + const response = await apiRoute.getStudentContracts({ + student_id: this.studentId, + status: this.activeStatus === 'all' ? '' : this.activeStatus, + page: this.currentPage, + limit: 10 + }) - if (mockResponse.code === 1) { - const newList = mockResponse.data.list || [] + if (response.code === 1) { + const newList = response.data.list || [] if (this.currentPage === 1) { this.contractsList = newList } else { this.contractsList = [...this.contractsList, ...newList] } - this.hasMore = mockResponse.data.has_more || false - this.contractStats = mockResponse.data.stats || {} + this.hasMore = response.data.has_more || false + this.contractStats = response.data.stats || {} this.applyStatusFilter() console.log('合同数据加载成功:', this.contractsList) } else { - uni.showToast({ - title: mockResponse.msg || '获取合同列表失败', - icon: 'none' - }) + console.error('API返回错误:', response.msg) + // 如果API失败,使用模拟数据作为备用 + this.loadMockContracts() } } catch (error) { console.error('获取合同列表失败:', error) - uni.showToast({ - title: '获取合同列表失败', - icon: 'none' - }) + // 如果API失败,使用模拟数据作为备用 + this.loadMockContracts() } finally { this.loading = false this.loadingMore = false } }, + // 备用的模拟数据加载方法 + loadMockContracts() { + console.log('使用模拟数据') + const mockData = { + list: [ + { + sign_id: 1, + contract_id: 1, + contract_name: '少儿体适能训练合同', + course_type: '少儿体适能', + total_hours: 48, + remaining_hours: 32, + used_hours: 16, + total_amount: '4800.00', + status: 1, // 1=待签署 + status_text: '待签署', + sign_date: null, + create_date: '2024-01-15', + can_renew: false, + contract_file_url: '/uploads/contracts/contract_001.pdf' + } + ], + total: 1, + has_more: false, + stats: { + active_contracts: 0, + pending_contracts: 1, + remaining_hours: 32 + } + } + + if (this.currentPage === 1) { + this.contractsList = mockData.list + } else { + this.contractsList = [...this.contractsList, ...mockData.list] + } + + this.hasMore = mockData.has_more + this.contractStats = mockData.stats + this.applyStatusFilter() + }, + async loadMoreContracts(e) { // 阻止事件冒泡(如果事件存在) if (e && typeof e.stopPropagation === 'function') { @@ -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 { - this.filteredContracts = this.contractsList.filter(contract => contract.status === this.activeStatus) + // 状态映射:前端字符串状态 -> 后端数字状态 + 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 = {} - this.contractsList.forEach(contract => { - counts[contract.status] = (counts[contract.status] || 0) + 1 - }) - - this.statusTabs.forEach(tab => { - if (tab.value === 'all') { - tab.count = this.contractsList.length - } else { - tab.count = counts[tab.value] || 0 - } - }) + // 优先使用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 => { + switch(tab.value) { + case 'all': + tab.count = this.contractsList.length + 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) { - this.selectedContract = contract - this.showContractPopup = true + // 如果合同状态为未签署(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)}` }) } },