Browse Source

修改 bug

master
王泽彦 8 months ago
parent
commit
268470559b
  1. 138
      admin/src/app/views/contract/contract.vue
  2. 43
      niucloud/app/api/controller/student/ContractController.php
  3. 2
      niucloud/app/api/controller/student/KnowledgeController.php
  4. 2
      niucloud/app/api/route/student.php
  5. 6
      niucloud/app/service/admin/document/DocumentTemplateService.php
  6. 15
      niucloud/app/service/admin/student/StudentService.php
  7. 40
      niucloud/app/service/api/student/ContractService.php
  8. 39
      niucloud/app/service/api/student/KnowledgeService.php
  9. 2
      uniapp/common/config.js
  10. 73
      uniapp/pages-common/contract/contract_sign.vue
  11. 101
      uniapp/pages-student/contracts/index.vue
  12. 797
      uniapp/pages-student/contracts/sign.vue
  13. 8
      uniapp/pages-student/knowledge/index.vue
  14. 9
      uniapp/pages.json

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

@ -225,6 +225,7 @@
<th>占位符</th> <th>占位符</th>
<th>数据类型</th> <th>数据类型</th>
<th>数据配置</th> <th>数据配置</th>
<th>签署方</th>
<th>是否必填</th> <th>是否必填</th>
<th>默认值</th> <th>默认值</th>
</tr> </tr>
@ -238,6 +239,8 @@
<option value="database">数据库</option> <option value="database">数据库</option>
<option value="system">系统函数</option> <option value="system">系统函数</option>
<option value="user_input">用户输入</option> <option value="user_input">用户输入</option>
<option value="sign_img">签名图片</option>
<option value="signature">电子签名</option>
</select> </select>
</td> </td>
<td> <td>
@ -309,11 +312,29 @@
> >
</div> </div>
<!-- 签名图片类型配置 -->
<div v-else-if="config.data_type === 'sign_img'" class="data-config">
<span class="config-info">图片文件上传字段</span>
</div>
<!-- 电子签名类型配置 -->
<div v-else-if="config.data_type === 'signature'" class="data-config">
<span class="config-info">手写签名字段需跳转签名页面</span>
</div>
<!-- 未选择类型 --> <!-- 未选择类型 -->
<div v-else class="data-config-empty"> <div v-else class="data-config-empty">
<span class="placeholder-text">请先选择数据类型</span> <span class="placeholder-text">请先选择数据类型</span>
</div> </div>
</td> </td>
<td>
<!-- 签署方选择所有字段都支持甲方乙方选择 -->
<select v-model="config.sign_party" class="form-select">
<option value="">请选择</option>
<option value="party_a">甲方</option>
<option value="party_b">乙方</option>
</select>
</td>
<td> <td>
<input type="checkbox" v-model="config.is_required" :true-value="1" :false-value="0"> 必填 <input type="checkbox" v-model="config.is_required" :true-value="1" :false-value="0"> 必填
</td> </td>
@ -810,6 +831,8 @@ const loadPlaceholderConfig = async (contractId: number) => {
system_function: config.system_function || '', system_function: config.system_function || '',
// //
user_input_value: config.user_input_value || '', user_input_value: config.user_input_value || '',
//
sign_party: config.sign_party || '',
// //
field_type: config.field_type || 'text', field_type: config.field_type || 'text',
is_required: config.is_required || config.required || 0, is_required: config.is_required || config.required || 0,
@ -826,6 +849,7 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: config.field_name || '', field_name: config.field_name || '',
system_function: config.system_function || '', system_function: config.system_function || '',
user_input_value: config.user_input_value || '', user_input_value: config.user_input_value || '',
sign_party: config.sign_party || '',
field_type: config.field_type || 'text', field_type: config.field_type || 'text',
is_required: config.is_required || 0, is_required: config.is_required || 0,
default_value: config.default_value || '' default_value: config.default_value || ''
@ -841,6 +865,7 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: '', field_name: '',
system_function: '', system_function: '',
user_input_value: '', user_input_value: '',
sign_party: '',
field_type: 'text', field_type: 'text',
is_required: 0, is_required: 0,
default_value: '' default_value: ''
@ -856,6 +881,7 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: config.field_name || '', field_name: config.field_name || '',
system_function: config.system_function || '', system_function: config.system_function || '',
user_input_value: config.user_input_value || '', user_input_value: config.user_input_value || '',
sign_party: config.sign_party || '',
field_type: config.field_type || 'text', field_type: config.field_type || 'text',
is_required: config.is_required || 0, is_required: config.is_required || 0,
default_value: config.default_value || '' default_value: config.default_value || ''
@ -873,6 +899,7 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: 'real_name', field_name: 'real_name',
system_function: '', system_function: '',
user_input_value: '', user_input_value: '',
sign_party: 'party_b',
field_type: 'text', field_type: 'text',
is_required: 1, is_required: 1,
default_value: '' default_value: ''
@ -884,6 +911,7 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: 'amount', field_name: 'amount',
system_function: '', system_function: '',
user_input_value: '', user_input_value: '',
sign_party: 'party_a',
field_type: 'money', field_type: 'money',
is_required: 1, is_required: 1,
default_value: '' default_value: ''
@ -895,9 +923,34 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: '', field_name: '',
system_function: 'current_date', system_function: 'current_date',
user_input_value: '', user_input_value: '',
sign_party: 'party_b',
field_type: 'date', field_type: 'date',
is_required: 0, is_required: 0,
default_value: '2025-01-01' default_value: '2025-01-01'
},
{
placeholder: '{{学员签名}}',
data_type: 'signature',
table_name: '',
field_name: '',
system_function: '',
user_input_value: '',
sign_party: 'party_b',
field_type: 'signature',
is_required: 1,
default_value: ''
},
{
placeholder: '{{机构名称}}',
data_type: 'user_input',
table_name: '',
field_name: '',
system_function: '',
user_input_value: '某某培训机构',
sign_party: 'party_a',
field_type: 'text',
is_required: 1,
default_value: ''
} }
] ]
} }
@ -912,6 +965,7 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: 'real_name', field_name: 'real_name',
system_function: '', system_function: '',
user_input_value: '', user_input_value: '',
sign_party: 'party_b',
field_type: 'text', field_type: 'text',
is_required: 1, is_required: 1,
default_value: '' default_value: ''
@ -923,6 +977,7 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: 'amount', field_name: 'amount',
system_function: '', system_function: '',
user_input_value: '', user_input_value: '',
sign_party: 'party_a',
field_type: 'money', field_type: 'money',
is_required: 1, is_required: 1,
default_value: '' default_value: ''
@ -934,9 +989,34 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: '', field_name: '',
system_function: 'current_date', system_function: 'current_date',
user_input_value: '', user_input_value: '',
sign_party: 'party_b',
field_type: 'date', field_type: 'date',
is_required: 0, is_required: 0,
default_value: '2025-01-01' default_value: '2025-01-01'
},
{
placeholder: '{{学员签名}}',
data_type: 'signature',
table_name: '',
field_name: '',
system_function: '',
user_input_value: '',
sign_party: 'party_b',
field_type: 'signature',
is_required: 1,
default_value: ''
},
{
placeholder: '{{机构名称}}',
data_type: 'user_input',
table_name: '',
field_name: '',
system_function: '',
user_input_value: '某某培训机构',
sign_party: 'party_a',
field_type: 'text',
is_required: 1,
default_value: ''
} }
] ]
} }
@ -954,6 +1034,7 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: 'real_name', field_name: 'real_name',
system_function: '', system_function: '',
user_input_value: '', user_input_value: '',
sign_party: 'party_b',
field_type: 'text', field_type: 'text',
is_required: 1, is_required: 1,
default_value: '' default_value: ''
@ -965,6 +1046,7 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: 'amount', field_name: 'amount',
system_function: '', system_function: '',
user_input_value: '', user_input_value: '',
sign_party: 'party_a',
field_type: 'money', field_type: 'money',
is_required: 1, is_required: 1,
default_value: '' default_value: ''
@ -976,9 +1058,34 @@ const loadPlaceholderConfig = async (contractId: number) => {
field_name: '', field_name: '',
system_function: 'current_date', system_function: 'current_date',
user_input_value: '', user_input_value: '',
sign_party: 'party_b',
field_type: 'date', field_type: 'date',
is_required: 0, is_required: 0,
default_value: '2025-01-01' default_value: '2025-01-01'
},
{
placeholder: '{{学员签名}}',
data_type: 'signature',
table_name: '',
field_name: '',
system_function: '',
user_input_value: '',
sign_party: 'party_b',
field_type: 'signature',
is_required: 1,
default_value: ''
},
{
placeholder: '{{机构名称}}',
data_type: 'user_input',
table_name: '',
field_name: '',
system_function: '',
user_input_value: '某某培训机构',
sign_party: 'party_a',
field_type: 'text',
is_required: 1,
default_value: ''
} }
] ]
} finally { } finally {
@ -1001,6 +1108,9 @@ const onDataTypeChange = (config: any) => {
if (config.data_type !== 'user_input') { if (config.data_type !== 'user_input') {
config.user_input_value = '' config.user_input_value = ''
} }
//
// config.sign_party
} }
// //
@ -1015,7 +1125,7 @@ const handleConfigSuccess = async () => {
console.log('💾 开始保存占位符配置...') console.log('💾 开始保存占位符配置...')
console.log('📋 配置数据:', configList.value) console.log('📋 配置数据:', configList.value)
// //
const saveData = { const saveData = {
template_id: currentContractId.value, template_id: currentContractId.value,
configs: configList.value.map(config => ({ configs: configList.value.map(config => ({
@ -1028,6 +1138,8 @@ const handleConfigSuccess = async () => {
system_function: config.data_type === 'system' ? config.system_function : '', system_function: config.data_type === 'system' ? config.system_function : '',
// //
user_input_value: config.data_type === 'user_input' ? config.user_input_value : '', user_input_value: config.data_type === 'user_input' ? config.user_input_value : '',
//
sign_party: config.sign_party || '',
// //
field_type: config.field_type || 'text', field_type: config.field_type || 'text',
is_required: config.is_required ? 1 : 0, is_required: config.is_required ? 1 : 0,
@ -1923,4 +2035,28 @@ onMounted(() => {
color: #409eff; color: #409eff;
border: 1px solid #b3d8ff; border: 1px solid #b3d8ff;
} }
/* 新增配置信息样式 */
.config-info {
color: #67c23a;
font-size: 12px;
padding: 4px 8px;
background: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
display: inline-block;
}
/* 签署方选择样式 */
.config-table td .form-select {
min-width: 100px;
}
/* 签名类型提示样式 */
.data-config .config-info {
white-space: nowrap;
text-align: center;
min-width: 150px;
}
</style> </style>

43
niucloud/app/api/controller/student/ContractController.php

@ -11,6 +11,7 @@ namespace app\api\controller\student;
use app\service\api\student\ContractService; use app\service\api\student\ContractService;
use core\base\BaseController; use core\base\BaseController;
use think\Request;
use think\Response; use think\Response;
/** /**
@ -107,29 +108,35 @@ class ContractController extends BaseController
* 提交合同签署 * 提交合同签署
* @return Response * @return Response
*/ */
public function signContract() public function signContract(Request $request)
{ {
$data = $this->request->params([ $contract_id = $this->request->param('contract_id', 0);
['contract_id', 0], $sign_file = $this->request->param('sign_file', '');
['student_id', 0],
['form_data', []],
['signature_image', '']
]);
$this->validate($data, [ if (empty($contract_id)) {
'contract_id' => 'require|integer|gt:0', return fail('合同ID不能为空');
'student_id' => 'require|integer|gt:0', }
'form_data' => 'require|array'
]);
if (empty($sign_file)) {
return fail('签名文件不能为空');
}
$data = [
'contract_id' => $contract_id,
'personnel_id' => $this->member_id,
'sign_file' => $sign_file
];
try { try {
$service = new ContractService(); $service = new \app\service\api\apiService\ContractService();
$result = $service->signContract($data); $res = $service->signContract($data);
return success($result, '合同签署成功'); if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data']);
} catch (\Exception $e) { } catch (\Exception $e) {
return fail($e->getMessage()); return fail('签订合同失败:' . $e->getMessage());
} }
} }

2
niucloud/app/api/controller/student/KnowledgeController.php

@ -55,7 +55,7 @@ class KnowledgeController extends BaseController
{ {
try { try {
$service = new KnowledgeService(); $service = new KnowledgeService();
$result = $service->getKnowledgeCategories(); $result = $service->getKnowledgeCategories('student');
return success($result, '获取知识分类成功'); return success($result, '获取知识分类成功');

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

@ -99,7 +99,7 @@ Route::group('contract', function () {
// 获取签署表单配置 // 获取签署表单配置
Route::get('sign-form/:contract_id', 'app\api\controller\student\ContractController@getSignForm'); Route::get('sign-form/:contract_id', 'app\api\controller\student\ContractController@getSignForm');
// 提交合同签署 // 提交合同签署
Route::post('sign', 'app\api\controller\student\ContractController@signContract'); Route::post('student/sign', 'app\api\controller\student\ContractController@signContract');
// 下载合同 // 下载合同
Route::get('download/:contract_id', 'app\api\controller\student\ContractController@downloadContract'); Route::get('download/:contract_id', 'app\api\controller\student\ContractController@downloadContract');
// 获取学员基本信息 // 获取学员基本信息

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

@ -94,6 +94,7 @@ class DocumentTemplateService extends BaseAdminService
'field_name' => $config['field_name'] ?? '', 'field_name' => $config['field_name'] ?? '',
'system_function' => $config['system_function'] ?? '', 'system_function' => $config['system_function'] ?? '',
'user_input_value' => $config['user_input_value'] ?? '', 'user_input_value' => $config['user_input_value'] ?? '',
'sign_party' => $config['sign_party'] ?? '',
'field_type' => $config['field_type'] ?? 'text', 'field_type' => $config['field_type'] ?? 'text',
'is_required' => $config['is_required'] ?? 0, 'is_required' => $config['is_required'] ?? 0,
'default_value' => $config['default_value'] ?? '' 'default_value' => $config['default_value'] ?? ''
@ -115,6 +116,7 @@ class DocumentTemplateService extends BaseAdminService
'field_name' => '', 'field_name' => '',
'system_function' => '', 'system_function' => '',
'user_input_value' => '', 'user_input_value' => '',
'sign_party' => '',
'field_type' => 'text', 'field_type' => 'text',
'is_required' => 0, 'is_required' => 0,
'default_value' => '' 'default_value' => ''
@ -406,6 +408,7 @@ class DocumentTemplateService extends BaseAdminService
'field_name' => $config['field_name'] ?? '', 'field_name' => $config['field_name'] ?? '',
'system_function' => $config['system_function'] ?? '', 'system_function' => $config['system_function'] ?? '',
'user_input_value' => $config['user_input_value'] ?? '', 'user_input_value' => $config['user_input_value'] ?? '',
'sign_party' => $config['sign_party'] ?? '',
'field_type' => $config['field_type'] ?? 'text', 'field_type' => $config['field_type'] ?? 'text',
'is_required' => $config['is_required'] ?? 0, 'is_required' => $config['is_required'] ?? 0,
'default_value' => $config['default_value'] ?? '' 'default_value' => $config['default_value'] ?? ''
@ -454,6 +457,7 @@ class DocumentTemplateService extends BaseAdminService
'table_name' => $settings['table_name'] ?? '', 'table_name' => $settings['table_name'] ?? '',
'field_name' => $settings['field_name'] ?? '', 'field_name' => $settings['field_name'] ?? '',
'system_function' => $settings['system_function'] ?? '', 'system_function' => $settings['system_function'] ?? '',
'sign_party' => $settings['sign_party'] ?? '',
'field_type' => $settings['field_type'] ?? 'text', 'field_type' => $settings['field_type'] ?? 'text',
'is_required' => $settings['is_required'] ?? 0, 'is_required' => $settings['is_required'] ?? 0,
'default_value' => $settings['default_value'] ?? '', 'default_value' => $settings['default_value'] ?? '',
@ -1020,6 +1024,7 @@ class DocumentTemplateService extends BaseAdminService
'field_name' => '', 'field_name' => '',
'system_function' => '', 'system_function' => '',
'user_input_value' => '', 'user_input_value' => '',
'sign_party' => '',
'field_type' => 'text', 'field_type' => 'text',
'is_required' => 0, 'is_required' => 0,
'default_value' => '' 'default_value' => ''
@ -1145,6 +1150,7 @@ class DocumentTemplateService extends BaseAdminService
'field_name' => '', 'field_name' => '',
'system_function' => '', 'system_function' => '',
'user_input_value' => '', 'user_input_value' => '',
'sign_party' => '',
'field_type' => 'text', 'field_type' => 'text',
'is_required' => 0, 'is_required' => 0,
'default_value' => '' 'default_value' => ''

15
niucloud/app/service/admin/student/StudentService.php

@ -117,7 +117,20 @@ class StudentService extends BaseAdminService
{ {
//会员标签 //会员标签
if (!empty($data['member_label'])) { if (!empty($data['member_label'])) {
$data['member_label_array'] = (new StudentLabelService())->getMemberLabelListByLabelIds($data['member_label']); // 确保member_label是数组格式
$labelIds = $data['member_label'];
if (is_string($labelIds)) {
// 如果是JSON字符串,解析为数组
$labelIds = json_decode($labelIds, true) ?: [];
} elseif (is_numeric($labelIds)) {
// 如果是数字,转换为数组
$labelIds = [$labelIds];
} elseif (!is_array($labelIds)) {
// 其他情况设为空数组
$labelIds = [];
}
$data['member_label_array'] = (new StudentLabelService())->getMemberLabelListByLabelIds($labelIds);
} }
return $data; return $data;
} }

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

@ -191,31 +191,48 @@ class ContractService extends BaseService
// 获取合同基本信息 // 获取合同基本信息
$contract = Db::table('school_contract') $contract = Db::table('school_contract')
->where('id', $contractId) ->where('id', $contractId)
->field('id, contract_name, contract_type, contract_template, contract_content, placeholders')
->find(); ->find();
if (!$contract) { if (!$contract) {
throw new CommonException('合同模板不存在'); throw new CommonException('合同模板不存在');
} }
// 获取需要填写的字段配置 // 获取所有字段配置
$formFields = Db::table('school_document_data_source_config') $formFields = Db::table('school_document_data_source_config')
->where([ ->where('contract_id', $contractId)
['contract_id', '=', $contractId], ->field('id, placeholder, field_type, data_type, is_required, default_value, table_name, field_name, system_function, sign_party')
['data_type', '=', 'manual'] ->order('id ASC')
])
->field('placeholder, field_type, is_required, default_value')
->select() ->select()
->toArray(); ->toArray();
// 格式化表单字段 // 格式化表单字段
$fields = []; $fields = [];
foreach ($formFields as $field) { foreach ($formFields as $field) {
// 根据数据类型预填充默认值
$defaultValue = '';
switch ($field['data_type']) {
case 'database':
$defaultValue = $this->getDataFromDatabase($field['table_name'], $field['field_name'], $studentId);
break;
case 'system':
$defaultValue = $this->getSystemValue($field['system_function']);
break;
case 'user_input':
default:
$defaultValue = $field['default_value'] ?: '';
break;
}
$fields[] = [ $fields[] = [
'name' => $field['placeholder'], 'id' => $field['id'],
'type' => $field['field_type'], 'name' => $field['placeholder'], // 使用placeholder作为字段名称
'required' => (bool)$field['is_required'], 'placeholder' => $field['placeholder'],
'default_value' => $field['default_value'] ?: '', 'field_type' => $field['field_type'],
'placeholder' => '请输入' . $field['placeholder'] 'data_type' => $field['data_type'],
'is_required' => (bool)$field['is_required'],
'default_value' => $defaultValue,
'sign_party' => $field['sign_party']
]; ];
} }
@ -223,6 +240,7 @@ class ContractService extends BaseService
'contract_id' => $contractId, 'contract_id' => $contractId,
'contract_name' => $contract['contract_name'], 'contract_name' => $contract['contract_name'],
'contract_type' => $contract['contract_type'], 'contract_type' => $contract['contract_type'],
'contract_content' => $contract['contract_content'] ?: '',
'form_fields' => $fields, 'form_fields' => $fields,
'contract_template_url' => $contract['contract_template'] ? get_image_url($contract['contract_template']) : '' 'contract_template_url' => $contract['contract_template'] ? get_image_url($contract['contract_template']) : ''
]; ];

39
niucloud/app/service/api/student/KnowledgeService.php

@ -48,7 +48,9 @@ class KnowledgeService extends BaseService
'28' => ['name' => '套圈游戏', 'icon' => '⭕'], '28' => ['name' => '套圈游戏', 'icon' => '⭕'],
'29' => ['name' => '鼓励方式', 'icon' => '👏'] '29' => ['name' => '鼓励方式', 'icon' => '👏']
]; ];
private $typeArr = [
'student'=>['13','14','15','27','28','29']
];
/** /**
* 获取知识文章列表 * 获取知识文章列表
* @param array $data * @param array $data
@ -139,7 +141,7 @@ class KnowledgeService extends BaseService
* 获取知识分类列表 * 获取知识分类列表
* @return array * @return array
*/ */
public function getKnowledgeCategories() public function getKnowledgeCategories($type)
{ {
$categories = []; $categories = [];
@ -151,18 +153,33 @@ class KnowledgeService extends BaseService
->field('table_type, count(*) as count') ->field('table_type, count(*) as count')
->select(); ->select();
foreach ($stats as $stat) { if ($type === 'student'){
$tableType = $stat['table_type']; foreach ($stats as $stat) {
if (isset($this->tableTypeMap[$tableType])) { $tableType = $stat['table_type'];
$categories[] = [ if (in_array($tableType,$this->typeArr[$type])){
'value' => $tableType, $categories[] = [
'text' => $this->tableTypeMap[$tableType]['name'], 'value' => $tableType,
'icon' => $this->tableTypeMap[$tableType]['icon'], 'text' => $this->tableTypeMap[$tableType]['name'],
'count' => $stat['count'] 'icon' => $this->tableTypeMap[$tableType]['icon'],
]; 'count' => $stat['count']
];
}
}
}else{
foreach ($stats as $stat) {
$tableType = $stat['table_type'];
if (isset($this->tableTypeMap[$tableType])){
$categories[] = [
'value' => $tableType,
'text' => $this->tableTypeMap[$tableType]['name'],
'icon' => $this->tableTypeMap[$tableType]['icon'],
'count' => $stat['count']
];
}
} }
} }
return $categories; return $categories;
} }

2
uniapp/common/config.js

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

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

@ -10,8 +10,10 @@
<!-- 合同信息 --> <!-- 合同信息 -->
<view class="contract-info"> <view class="contract-info">
<view class="info-card"> <view class="info-card">
<text class="contract-name">{{contractName}}</text> <text class="contract-name" v-if="isFormField && fieldName">{{fieldName}}</text>
<text class="sign-tip">请在下方签名区域进行手写签名</text> <text class="contract-name" v-else>{{contractName}}</text>
<text class="sign-tip" v-if="isFormField">请为此字段进行手写签名</text>
<text class="sign-tip" v-else>请在下方签名区域进行手写签名</text>
</view> </view>
</view> </view>
@ -85,7 +87,7 @@
@click="submitSignature" @click="submitSignature"
:disabled="!hasSigned || submitting" :disabled="!hasSigned || submitting"
:loading="submitting"> :loading="submitting">
{{submitting ? '提交中...' : '确认签订'}} {{submitting ? '提交中...' : (isFormField ? '确认签名' : '确认签订')}}
</button> </button>
</view> </view>
@ -120,6 +122,9 @@ export default {
return { return {
contractId: 0, contractId: 0,
contractName: '', contractName: '',
fieldName: '', //
fieldPlaceholder: '', //
isFormField: false, //
canvas: null, canvas: null,
ctx: null, ctx: null,
isDrawing: false, isDrawing: false,
@ -142,13 +147,30 @@ export default {
} }
}, },
onLoad(options) { onLoad(options) {
//
if (options.id) { if (options.id) {
//
this.contractId = parseInt(options.id) this.contractId = parseInt(options.id)
this.isFormField = false
} }
if (options.contract_id) {
//
this.contractId = parseInt(options.contract_id)
this.isFormField = true
}
if (options.contractName) { if (options.contractName) {
this.contractName = decodeURIComponent(options.contractName) this.contractName = decodeURIComponent(options.contractName)
} }
if (options.field_name) {
this.fieldName = decodeURIComponent(options.field_name)
}
if (options.field_placeholder) {
this.fieldPlaceholder = decodeURIComponent(options.field_placeholder)
}
this.$nextTick(() => { this.$nextTick(() => {
this.initCanvas() this.initCanvas()
}) })
@ -348,7 +370,13 @@ export default {
} }
this.generateSignatureImage(() => { this.generateSignatureImage(() => {
this.uploadSignature() if (this.isFormField) {
// -
this.returnSignatureData()
} else {
// -
this.uploadSignature()
}
}) })
}, },
@ -404,6 +432,43 @@ export default {
this.submitting = false this.submitting = false
} }
}, },
//
returnSignatureData() {
if (!this.signatureImageUrl) {
uni.showToast({
title: '签名生成失败,请重试',
icon: 'none'
})
return
}
//
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage) {
//
if (!prevPage.data) {
prevPage.data = {}
}
prevPage.data.newSignature = {
fieldPlaceholder: this.fieldPlaceholder,
signatureData: this.signatureImageUrl
}
}
uni.showToast({
title: '签名完成',
icon: 'success',
duration: 1500
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
},
// //
async uploadSignatureFile() { async uploadSignatureFile() {

101
uniapp/pages-student/contracts/index.vue

@ -48,10 +48,10 @@
<view v-else class="contracts_list"> <view v-else class="contracts_list">
<view <view
v-for="contract in filteredContracts" v-for="(contract,index) in filteredContracts"
:key="contract.id" :key="contract.sign_id || index"
class="contract_item" class="contract_item"
@click="viewContractDetail(contract)" @click="handleContractClick(index)"
> >
<view class="contract_header"> <view class="contract_header">
<view class="contract_info"> <view class="contract_info">
@ -333,6 +333,20 @@
if (response.code === 1) { if (response.code === 1) {
const newList = response.data.list || [] const newList = response.data.list || []
console.log('API返回的合同列表:', newList)
console.log('API返回的完整响应:', response)
//
newList.forEach((contract, index) => {
console.log(`合同 ${index} 完整数据:`, JSON.stringify(contract, null, 2))
if (!contract.status) {
console.warn(`合同 ${index} 缺少status字段:`, contract)
}
if (!contract.contract_id) {
console.warn(`合同 ${index} 缺少contract_id字段:`, contract)
}
})
if (this.currentPage === 1) { if (this.currentPage === 1) {
this.contractsList = newList this.contractsList = newList
} else { } else {
@ -342,7 +356,7 @@
this.hasMore = response.data.has_more || false this.hasMore = response.data.has_more || false
this.contractStats = response.data.stats || {} this.contractStats = response.data.stats || {}
this.applyStatusFilter() this.applyStatusFilter()
console.log('合同数据加载成功:', this.contractsList) console.log('合同数据加载成功,处理后的列表:', this.contractsList)
} else { } else {
uni.showToast({ uni.showToast({
title: response.msg || '获取合同列表失败', title: response.msg || '获取合同列表失败',
@ -437,6 +451,85 @@
return `${year}-${month}-${day}` return `${year}-${month}-${day}`
}, },
//
handleContractClick(index) {
let contract = this.filteredContracts[index]
console.log('点击合同:', this.filteredContracts[index])
if (!contract) {
console.error('合同对象为空')
uni.showToast({
title: '合同数据错误',
icon: 'none'
})
return
}
console.log('点击合同:', contract)
console.log('合同字段检查:', {
sign_id: contract.sign_id,
contract_id: contract.contract_id,
status: contract.status,
contract_name: contract.contract_name
})
// 访typeof
const contractId = contract.contract_id
const statusValue = contract.status
if (!contractId) {
console.error('合同ID缺失')
uni.showToast({
title: '合同ID信息缺失',
icon: 'none'
})
return
}
if (statusValue === null || statusValue === undefined) {
console.error('合同状态缺失')
uni.showToast({
title: '合同状态信息缺失',
icon: 'none'
})
return
}
// status
const status = parseInt(statusValue)
if (status === 1) {
// -
this.goToSignPage(contract)
} else {
// -
this.viewContractDetail(contract)
}
},
//
goToSignPage(contract) {
if (!contract || !contract.contract_id) {
uni.showToast({
title: '合同信息错误',
icon: 'none'
})
return
}
const url = `/pages-student/contracts/sign?contract_id=${contract.contract_id}&student_id=${this.studentId}&contract_name=${encodeURIComponent(contract.contract_name || '')}`
uni.navigateTo({
url: url,
fail: (err) => {
console.error('页面跳转失败:', err)
uni.showToast({
title: '页面跳转失败',
icon: 'none'
})
}
})
},
async viewContractDetail(contract) { async viewContractDetail(contract) {
try { try {
uni.showLoading({ title: '加载中...' }) uni.showLoading({ title: '加载中...' })

797
uniapp/pages-student/contracts/sign.vue

@ -0,0 +1,797 @@
<!--合同签署表单页面-->
<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">
<fui-button
type="warning"
btn-size="medium"
width="100%"
:loading="submitting"
@click="submitContract"
>
{{ submitting ? '提交中...' : '提交签署' }}
</fui-button>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
contractId: 0,
studentId: 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.studentId = parseInt(options.student_id) || 0
this.contractName = decodeURIComponent(options.contract_name || '')
if (this.contractId && this.studentId) {
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, studentId: this.studentId })
//
const response = await apiRoute.getStudentContractSignForm({
contract_id: this.contractId,
student_id: this.studentId
})
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}`
})
},
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/upload/image',
filePath: tempFilePath,
name: 'image',
header: {
'Authorization': 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,
studentId: this.studentId,
formData: this.formData
})
//
const response = await apiRoute.signStudentContract({
contract_id: this.contractId,
student_id: this.studentId,
form_data: this.formData
})
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
}
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
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: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 24rpx;
.info_header {
.contract_name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.contract_type {
font-size: 24rpx;
color: #666;
}
}
}
//
.contract_content_section {
background: #fff;
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: #333;
}
.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: #666;
line-height: 1.6;
word-break: break-all;
}
}
}
//
.form_section {
margin: 20rpx;
.form_title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
padding: 0 8rpx;
}
.form_content {
.form_field {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.field_label {
font-size: 28rpx;
color: #333;
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 #e0e0e0;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
background: #fff;
&::placeholder {
color: #999;
}
&.disabled {
background: #f5f5f5;
color: #666;
}
}
}
.field_textarea {
textarea {
width: 100%;
min-height: 120rpx;
padding: 20rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
background: #fff;
&::placeholder {
color: #999;
}
&.disabled {
background: #f5f5f5;
color: #666;
}
}
}
.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 #e0e0e0;
}
}
}
.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 #e0e0e0;
}
}
}
.field_hint {
margin-top: 8rpx;
font-size: 22rpx;
color: #999;
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 #f0f0f0;
border-left: 4rpx solid #29D3B4;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
.loading_text {
font-size: 28rpx;
color: #666;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
//
.submit_section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 32rpx 40rpx;
border-top: 1rpx solid #f0f0f0;
//
// #ifdef MP-WEIXIN
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
// #endif
}
</style>

8
uniapp/pages-student/knowledge/index.vue

@ -192,7 +192,7 @@
<script> <script>
import apiRoute from '@/api/apiRoute.js' import apiRoute from '@/api/apiRoute.js'
import studentRoute from '@/api/studentRoute.js' import studentRoute from '@/api/member.js'
export default { export default {
data() { data() {
@ -373,9 +373,7 @@
async loadStudentInfo() { async loadStudentInfo() {
try { try {
// API // API
const response = await apiRoute.xy_memberInfo({ const response = await studentRoute.getStudentInfo(this.studentId)
student_id: this.studentId
})
if (response && response.code === 1 && response.data) { if (response && response.code === 1 && response.data) {
this.studentInfo = { this.studentInfo = {
@ -430,7 +428,7 @@
} }
// //
this.hasMore = apiData.current_page < apiData.last_page this.hasMore = parseInt(apiData.current_page) < parseInt(apiData.last_page)
// API // API
if (apiData.stats) { if (apiData.stats) {

9
uniapp/pages.json

@ -128,6 +128,15 @@
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
} }
}, },
{
"path": "contracts/sign",
"style": {
"navigationBarTitleText": "合同签署",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
},
{ {
"path": "knowledge/index", "path": "knowledge/index",
"style": { "style": {

Loading…
Cancel
Save