12 changed files with 3809 additions and 27 deletions
@ -0,0 +1,188 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | Niucloud-admin 企业快速开发的多应用管理平台 |
|||
// +---------------------------------------------------------------------- |
|||
// | 官方网址:https://www.niucloud.com |
|||
// +---------------------------------------------------------------------- |
|||
// | niucloud团队 版权所有 开源版本可自由商用 |
|||
// +---------------------------------------------------------------------- |
|||
// | Author: Niucloud Team |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
namespace app\api\controller\apiController; |
|||
|
|||
use app\Request; |
|||
use app\service\api\apiService\ContractService; |
|||
use core\base\BaseApiService; |
|||
|
|||
/** |
|||
* 合同管理控制器 |
|||
* Class Contract |
|||
* @package app\api\controller\apiController |
|||
*/ |
|||
class Contract extends BaseApiService |
|||
{ |
|||
/** |
|||
* 获取我的合同列表 |
|||
* @param Request $request |
|||
* @return mixed |
|||
*/ |
|||
public function myContracts(Request $request) |
|||
{ |
|||
$page = $request->param('page', 1); |
|||
$limit = $request->param('limit', 10); |
|||
|
|||
$where = [ |
|||
'personnel_id' => $this->member_id, |
|||
'page' => $page, |
|||
'limit' => $limit |
|||
]; |
|||
|
|||
try { |
|||
$service = new ContractService(); |
|||
$res = $service->getMyContracts($where); |
|||
|
|||
if (!$res['code']) { |
|||
return fail($res['msg']); |
|||
} |
|||
|
|||
return success($res['data']); |
|||
} catch (\Exception $e) { |
|||
return fail('获取合同列表失败:' . $e->getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取合同详情 |
|||
* @param Request $request |
|||
* @return mixed |
|||
*/ |
|||
public function detail(Request $request) |
|||
{ |
|||
$contract_id = $request->param('id', 0); |
|||
|
|||
if (empty($contract_id)) { |
|||
return fail('合同ID不能为空'); |
|||
} |
|||
|
|||
$where = [ |
|||
'contract_id' => $contract_id, |
|||
'personnel_id' => $this->member_id |
|||
]; |
|||
|
|||
try { |
|||
$service = new ContractService(); |
|||
$res = $service->getContractDetail($where); |
|||
|
|||
if (!$res['code']) { |
|||
return fail($res['msg']); |
|||
} |
|||
|
|||
return success($res['data']); |
|||
} catch (\Exception $e) { |
|||
return fail('获取合同详情失败:' . $e->getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 签订合同 |
|||
* @param Request $request |
|||
* @return mixed |
|||
*/ |
|||
public function sign(Request $request) |
|||
{ |
|||
$contract_id = $request->param('contract_id', 0); |
|||
$sign_file = $request->param('sign_file', ''); |
|||
|
|||
if (empty($contract_id)) { |
|||
return fail('合同ID不能为空'); |
|||
} |
|||
|
|||
if (empty($sign_file)) { |
|||
return fail('签名文件不能为空'); |
|||
} |
|||
|
|||
$data = [ |
|||
'contract_id' => $contract_id, |
|||
'personnel_id' => $this->member_id, |
|||
'sign_file' => $sign_file |
|||
]; |
|||
|
|||
try { |
|||
$service = new ContractService(); |
|||
$res = $service->signContract($data); |
|||
|
|||
if (!$res['code']) { |
|||
return fail($res['msg']); |
|||
} |
|||
|
|||
return success($res['data']); |
|||
} catch (\Exception $e) { |
|||
return fail('签订合同失败:' . $e->getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取合同签订状态 |
|||
* @param Request $request |
|||
* @return mixed |
|||
*/ |
|||
public function signStatus(Request $request) |
|||
{ |
|||
$contract_id = $request->param('contract_id', 0); |
|||
|
|||
if (empty($contract_id)) { |
|||
return fail('合同ID不能为空'); |
|||
} |
|||
|
|||
$where = [ |
|||
'contract_id' => $contract_id, |
|||
'personnel_id' => $this->member_id |
|||
]; |
|||
|
|||
try { |
|||
$service = new ContractService(); |
|||
$res = $service->getSignStatus($where); |
|||
|
|||
if (!$res['code']) { |
|||
return fail($res['msg']); |
|||
} |
|||
|
|||
return success($res['data']); |
|||
} catch (\Exception $e) { |
|||
return fail('获取签订状态失败:' . $e->getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 下载合同文件 |
|||
* @param Request $request |
|||
* @return mixed |
|||
*/ |
|||
public function download(Request $request) |
|||
{ |
|||
$contract_id = $request->param('contract_id', 0); |
|||
|
|||
if (empty($contract_id)) { |
|||
return fail('合同ID不能为空'); |
|||
} |
|||
|
|||
$where = [ |
|||
'contract_id' => $contract_id, |
|||
'personnel_id' => $this->member_id |
|||
]; |
|||
|
|||
try { |
|||
$service = new ContractService(); |
|||
$res = $service->downloadContract($where); |
|||
|
|||
if (!$res['code']) { |
|||
return fail($res['msg']); |
|||
} |
|||
|
|||
return success($res['data']); |
|||
} catch (\Exception $e) { |
|||
return fail('下载合同失败:' . $e->getMessage()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,358 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | Niucloud-admin 企业快速开发的多应用管理平台 |
|||
// +---------------------------------------------------------------------- |
|||
// | 官方网址:https://www.niucloud.com |
|||
// +---------------------------------------------------------------------- |
|||
// | niucloud团队 版权所有 开源版本可自由商用 |
|||
// +---------------------------------------------------------------------- |
|||
// | Author: Niucloud Team |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
namespace app\service\api\apiService; |
|||
|
|||
use app\model\contract\Contract; |
|||
use app\model\contract_sign\ContractSign; |
|||
use core\base\BaseApiService; |
|||
use think\facade\Db; |
|||
|
|||
/** |
|||
* 合同服务层 |
|||
* Class ContractService |
|||
* @package app\service\api\apiService |
|||
*/ |
|||
class ContractService extends BaseApiService |
|||
{ |
|||
/** |
|||
* 获取我的合同列表 |
|||
* @param array $where |
|||
* @return array |
|||
*/ |
|||
public function getMyContracts(array $where) |
|||
{ |
|||
$res = [ |
|||
'code' => 0, |
|||
'msg' => '获取合同列表失败', |
|||
'data' => [] |
|||
]; |
|||
|
|||
try { |
|||
$page = $where['page'] ?? 1; |
|||
$limit = $where['limit'] ?? 10; |
|||
$personnel_id = $where['personnel_id'] ?? 0; |
|||
|
|||
if (empty($personnel_id)) { |
|||
$res['msg'] = '员工ID不能为空'; |
|||
return $res; |
|||
} |
|||
|
|||
// 查询合同签订记录,关联合同表 |
|||
$contractSignModel = new ContractSign(); |
|||
$list = $contractSignModel->alias('cs') |
|||
->join('school_contract c', 'cs.contract_id = c.id') |
|||
->where('cs.personnel_id', $personnel_id) |
|||
->where('cs.deleted_at', 0) |
|||
->where('c.deleted_at', 0) |
|||
->field([ |
|||
'cs.id', |
|||
'cs.contract_id', |
|||
'cs.personnel_id', |
|||
'cs.sign_file', |
|||
'cs.status', |
|||
'cs.created_at', |
|||
'cs.sign_time', |
|||
'c.contract_name', |
|||
'c.contract_template', |
|||
'c.contract_status', |
|||
'c.contract_type', |
|||
'c.remarks' |
|||
]) |
|||
->order('cs.created_at', 'desc') |
|||
->paginate([ |
|||
'list_rows' => $limit, |
|||
'page' => $page |
|||
]) |
|||
->toArray(); |
|||
|
|||
$res = [ |
|||
'code' => 1, |
|||
'msg' => '获取成功', |
|||
'data' => $list |
|||
]; |
|||
|
|||
} catch (\Exception $e) { |
|||
$res['msg'] = '获取合同列表异常:' . $e->getMessage(); |
|||
} |
|||
|
|||
return $res; |
|||
} |
|||
|
|||
/** |
|||
* 获取合同详情 |
|||
* @param array $where |
|||
* @return array |
|||
*/ |
|||
public function getContractDetail(array $where) |
|||
{ |
|||
$res = [ |
|||
'code' => 0, |
|||
'msg' => '获取合同详情失败', |
|||
'data' => [] |
|||
]; |
|||
|
|||
try { |
|||
$contract_id = $where['contract_id'] ?? 0; |
|||
$personnel_id = $where['personnel_id'] ?? 0; |
|||
|
|||
if (empty($contract_id) || empty($personnel_id)) { |
|||
$res['msg'] = '参数错误'; |
|||
return $res; |
|||
} |
|||
|
|||
// 查询合同签订记录,关联合同表 |
|||
$contractSign = ContractSign::alias('cs') |
|||
->join('school_contract c', 'cs.contract_id = c.id') |
|||
->where('cs.contract_id', $contract_id) |
|||
->where('cs.personnel_id', $personnel_id) |
|||
->where('cs.deleted_at', 0) |
|||
->where('c.deleted_at', 0) |
|||
->field([ |
|||
'cs.id', |
|||
'cs.contract_id', |
|||
'cs.personnel_id', |
|||
'cs.sign_file', |
|||
'cs.status', |
|||
'cs.created_at', |
|||
'cs.sign_time', |
|||
'cs.updated_at', |
|||
'c.contract_name', |
|||
'c.contract_template', |
|||
'c.contract_status', |
|||
'c.contract_type', |
|||
'c.remarks' |
|||
]) |
|||
->find(); |
|||
|
|||
if (empty($contractSign)) { |
|||
$res['msg'] = '合同不存在或无权限访问'; |
|||
return $res; |
|||
} |
|||
|
|||
$contractData = $contractSign->toArray(); |
|||
|
|||
$res = [ |
|||
'code' => 1, |
|||
'msg' => '获取成功', |
|||
'data' => $contractData |
|||
]; |
|||
|
|||
} catch (\Exception $e) { |
|||
$res['msg'] = '获取合同详情异常:' . $e->getMessage(); |
|||
} |
|||
|
|||
return $res; |
|||
} |
|||
|
|||
/** |
|||
* 签订合同 |
|||
* @param array $data |
|||
* @return array |
|||
*/ |
|||
public function signContract(array $data) |
|||
{ |
|||
$res = [ |
|||
'code' => 0, |
|||
'msg' => '签订合同失败', |
|||
'data' => [] |
|||
]; |
|||
|
|||
try { |
|||
$contract_id = $data['contract_id'] ?? 0; |
|||
$personnel_id = $data['personnel_id'] ?? 0; |
|||
$sign_file = $data['sign_file'] ?? ''; |
|||
|
|||
if (empty($contract_id) || empty($personnel_id) || empty($sign_file)) { |
|||
$res['msg'] = '参数错误'; |
|||
return $res; |
|||
} |
|||
|
|||
// 开启事务 |
|||
Db::startTrans(); |
|||
|
|||
// 查询合同签订记录 |
|||
$contractSign = ContractSign::where('contract_id', $contract_id) |
|||
->where('personnel_id', $personnel_id) |
|||
->where('deleted_at', 0) |
|||
->find(); |
|||
|
|||
if (empty($contractSign)) { |
|||
Db::rollback(); |
|||
$res['msg'] = '合同签订记录不存在'; |
|||
return $res; |
|||
} |
|||
|
|||
// 检查合同状态 |
|||
if ($contractSign['status'] != 1) { |
|||
Db::rollback(); |
|||
$res['msg'] = '合同状态不允许签订'; |
|||
return $res; |
|||
} |
|||
|
|||
// 检查是否已经签订 |
|||
if (!empty($contractSign['sign_file'])) { |
|||
Db::rollback(); |
|||
$res['msg'] = '合同已经签订,无需重复签订'; |
|||
return $res; |
|||
} |
|||
|
|||
// 更新签订信息 |
|||
$updateData = [ |
|||
'sign_file' => $sign_file, |
|||
'sign_time' => date('Y-m-d H:i:s'), |
|||
'updated_at' => date('Y-m-d H:i:s') |
|||
]; |
|||
|
|||
$updateResult = ContractSign::where('id', $contractSign['id'])->update($updateData); |
|||
|
|||
if (!$updateResult) { |
|||
Db::rollback(); |
|||
$res['msg'] = '更新签订信息失败'; |
|||
return $res; |
|||
} |
|||
|
|||
// 提交事务 |
|||
Db::commit(); |
|||
|
|||
$res = [ |
|||
'code' => 1, |
|||
'msg' => '签订成功', |
|||
'data' => [ |
|||
'contract_id' => $contract_id, |
|||
'sign_time' => $updateData['sign_time'] |
|||
] |
|||
]; |
|||
|
|||
} catch (\Exception $e) { |
|||
Db::rollback(); |
|||
$res['msg'] = '签订合同异常:' . $e->getMessage(); |
|||
} |
|||
|
|||
return $res; |
|||
} |
|||
|
|||
/** |
|||
* 获取合同签订状态 |
|||
* @param array $where |
|||
* @return array |
|||
*/ |
|||
public function getSignStatus(array $where) |
|||
{ |
|||
$res = [ |
|||
'code' => 0, |
|||
'msg' => '获取签订状态失败', |
|||
'data' => [] |
|||
]; |
|||
|
|||
try { |
|||
$contract_id = $where['contract_id'] ?? 0; |
|||
$personnel_id = $where['personnel_id'] ?? 0; |
|||
|
|||
if (empty($contract_id) || empty($personnel_id)) { |
|||
$res['msg'] = '参数错误'; |
|||
return $res; |
|||
} |
|||
|
|||
$contractSign = ContractSign::where('contract_id', $contract_id) |
|||
->where('personnel_id', $personnel_id) |
|||
->where('deleted_at', 0) |
|||
->field('id,sign_file,status,sign_time') |
|||
->find(); |
|||
|
|||
if (empty($contractSign)) { |
|||
$res['msg'] = '合同签订记录不存在'; |
|||
return $res; |
|||
} |
|||
|
|||
$signData = $contractSign->toArray(); |
|||
|
|||
// 判断签订状态 |
|||
$signData['is_signed'] = !empty($signData['sign_file']); |
|||
$signData['can_sign'] = $signData['status'] == 1 && empty($signData['sign_file']); |
|||
|
|||
$res = [ |
|||
'code' => 1, |
|||
'msg' => '获取成功', |
|||
'data' => $signData |
|||
]; |
|||
|
|||
} catch (\Exception $e) { |
|||
$res['msg'] = '获取签订状态异常:' . $e->getMessage(); |
|||
} |
|||
|
|||
return $res; |
|||
} |
|||
|
|||
/** |
|||
* 下载合同文件 |
|||
* @param array $where |
|||
* @return array |
|||
*/ |
|||
public function downloadContract(array $where) |
|||
{ |
|||
$res = [ |
|||
'code' => 0, |
|||
'msg' => '下载合同失败', |
|||
'data' => [] |
|||
]; |
|||
|
|||
try { |
|||
$contract_id = $where['contract_id'] ?? 0; |
|||
$personnel_id = $where['personnel_id'] ?? 0; |
|||
|
|||
if (empty($contract_id) || empty($personnel_id)) { |
|||
$res['msg'] = '参数错误'; |
|||
return $res; |
|||
} |
|||
|
|||
// 查询合同信息 |
|||
$contractSign = ContractSign::alias('cs') |
|||
->join('school_contract c', 'cs.contract_id = c.id') |
|||
->where('cs.contract_id', $contract_id) |
|||
->where('cs.personnel_id', $personnel_id) |
|||
->where('cs.deleted_at', 0) |
|||
->where('c.deleted_at', 0) |
|||
->field([ |
|||
'cs.sign_file', |
|||
'c.contract_template', |
|||
'c.contract_name' |
|||
]) |
|||
->find(); |
|||
|
|||
if (empty($contractSign)) { |
|||
$res['msg'] = '合同不存在或无权限访问'; |
|||
return $res; |
|||
} |
|||
|
|||
$contractData = $contractSign->toArray(); |
|||
|
|||
// 返回下载信息 |
|||
$downloadData = [ |
|||
'contract_name' => $contractData['contract_name'], |
|||
'sign_file' => $contractData['sign_file'], |
|||
'contract_template' => $contractData['contract_template'], |
|||
'download_url' => !empty($contractData['sign_file']) ? $contractData['sign_file'] : $contractData['contract_template'] |
|||
]; |
|||
|
|||
$res = [ |
|||
'code' => 1, |
|||
'msg' => '获取下载信息成功', |
|||
'data' => $downloadData |
|||
]; |
|||
|
|||
} catch (\Exception $e) { |
|||
$res['msg'] = '获取下载信息异常:' . $e->getMessage(); |
|||
} |
|||
|
|||
return $res; |
|||
} |
|||
} |
|||
@ -0,0 +1,766 @@ |
|||
<template> |
|||
<view class="contract-detail-container"> |
|||
<!-- 自定义导航栏 --> |
|||
<view class="custom-nav"> |
|||
<view class="nav-left" @click="goBack"> |
|||
<text class="iconfont icon-arrow-left"></text> |
|||
</view> |
|||
<view class="nav-title">合同详情</view> |
|||
<view class="nav-right"> |
|||
<view v-if="contractData.sign_file" class="share-btn" @click="shareContract"> |
|||
<text class="iconfont icon-share"></text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 加载状态 --> |
|||
<view v-if="loading" class="loading-container"> |
|||
<uni-load-more status="loading" content-text="加载中..."></uni-load-more> |
|||
</view> |
|||
|
|||
<!-- 合同内容 --> |
|||
<view v-else class="contract-content"> |
|||
<!-- 合同状态卡片 --> |
|||
<view class="status-card"> |
|||
<view class="status-header"> |
|||
<view class="status-info"> |
|||
<text class="contract-name">{{contractData.contract_name}}</text> |
|||
<view class="status-badge" :class="getStatusClass()"> |
|||
{{getStatusText()}} |
|||
</view> |
|||
</view> |
|||
<view class="status-icon"> |
|||
<text v-if="needSignButton()" class="status-icon-text">⏰</text> |
|||
<text v-else class="status-icon-text">✅</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="status-desc"> |
|||
<text v-if="needSignButton()" class="desc-text">请仔细阅读合同内容,确认无误后进行签订</text> |
|||
<text v-else class="desc-text">合同已签订完成,您可以查看或下载合同文件</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 合同基本信息 --> |
|||
<view class="info-section"> |
|||
<view class="section-title">基本信息</view> |
|||
<view class="info-list"> |
|||
<view class="info-item"> |
|||
<text class="info-label">合同名称</text> |
|||
<text class="info-value">{{contractData.contract_name}}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">合同类型</text> |
|||
<text class="info-value">{{contractData.contract_type}}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">合同状态</text> |
|||
<text class="info-value">{{contractData.contract_status}}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">创建时间</text> |
|||
<text class="info-value">{{formatDate(contractData.created_at)}}</text> |
|||
</view> |
|||
<view class="info-item" v-if="contractData.sign_time"> |
|||
<text class="info-label">签订时间</text> |
|||
<text class="info-value">{{formatDate(contractData.sign_time)}}</text> |
|||
</view> |
|||
<view class="info-item" v-if="contractData.remarks"> |
|||
<text class="info-label">备注</text> |
|||
<text class="info-value">{{contractData.remarks}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 合同模板预览 --> |
|||
<view class="template-section" v-if="contractData.contract_template"> |
|||
<view class="section-title">合同模板</view> |
|||
<view class="template-preview" @click="previewTemplate"> |
|||
<view class="template-icon"> |
|||
<text class="iconfont icon-file"></text> |
|||
</view> |
|||
<view class="template-info"> |
|||
<text class="template-name">{{getTemplateName()}}</text> |
|||
<text class="template-desc">点击预览合同模板</text> |
|||
</view> |
|||
<view class="template-arrow"> |
|||
<text class="iconfont icon-arrow-right"></text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 签名文件 --> |
|||
<view class="signature-section" v-if="contractData.sign_file"> |
|||
<view class="section-title">签名文件</view> |
|||
<view class="signature-preview" @click="previewSignature"> |
|||
<image :src="getSignatureUrl()" class="signature-image" mode="aspectFit"></image> |
|||
<view class="signature-overlay"> |
|||
<text class="signature-text">点击查看完整签名</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 操作历史 --> |
|||
<view class="history-section"> |
|||
<view class="section-title">操作历史</view> |
|||
<view class="history-list"> |
|||
<view class="history-item"> |
|||
<view class="history-dot"></view> |
|||
<view class="history-content"> |
|||
<text class="history-title">合同创建</text> |
|||
<text class="history-time">{{formatDate(contractData.created_at)}}</text> |
|||
</view> |
|||
</view> |
|||
<view class="history-item" v-if="contractData.sign_time"> |
|||
<view class="history-dot active"></view> |
|||
<view class="history-content"> |
|||
<text class="history-title">合同签订</text> |
|||
<text class="history-time">{{formatDate(contractData.sign_time)}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 底部操作按钮 --> |
|||
<view class="footer-actions" v-if="!loading"> |
|||
<button v-if="needSignButton()" |
|||
class="action-btn primary" |
|||
@click="goToSign"> |
|||
立即签订 |
|||
</button> |
|||
<button v-else |
|||
class="action-btn secondary" |
|||
@click="downloadContract"> |
|||
下载合同 |
|||
</button> |
|||
</view> |
|||
|
|||
<!-- 图片预览模态框 --> |
|||
<view v-if="showImagePreview" class="image-preview-modal" @click="closeImagePreview"> |
|||
<view class="preview-content" @click.stop> |
|||
<image :src="previewImageUrl" class="preview-image" mode="aspectFit"></image> |
|||
<view class="preview-close" @click="closeImagePreview"> |
|||
<text class="iconfont icon-close"></text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import apiRoute from '@/common/axios.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
contractId: 0, |
|||
contractData: {}, |
|||
loading: true, |
|||
showImagePreview: false, |
|||
previewImageUrl: '' |
|||
} |
|||
}, |
|||
onLoad(options) { |
|||
if (options.id) { |
|||
this.contractId = parseInt(options.id) |
|||
this.loadContractDetail() |
|||
} else { |
|||
uni.showToast({ |
|||
title: '合同ID不能为空', |
|||
icon: 'none' |
|||
}) |
|||
setTimeout(() => { |
|||
uni.navigateBack() |
|||
}, 1500) |
|||
} |
|||
}, |
|||
methods: { |
|||
// 返回上一页 |
|||
goBack() { |
|||
uni.navigateBack() |
|||
}, |
|||
|
|||
// 加载合同详情 |
|||
async loadContractDetail() { |
|||
this.loading = true |
|||
|
|||
try { |
|||
const response = await apiRoute.get('contract/detail', { |
|||
id: this.contractId |
|||
}) |
|||
|
|||
if (response.data.code === 1) { |
|||
this.contractData = response.data.data || {} |
|||
} else { |
|||
uni.showToast({ |
|||
title: response.data.msg || '加载失败', |
|||
icon: 'none' |
|||
}) |
|||
setTimeout(() => { |
|||
uni.navigateBack() |
|||
}, 1500) |
|||
} |
|||
} catch (error) { |
|||
console.error('加载合同详情失败:', error) |
|||
uni.showToast({ |
|||
title: '网络错误,请稍后重试', |
|||
icon: 'none' |
|||
}) |
|||
} finally { |
|||
this.loading = false |
|||
} |
|||
}, |
|||
|
|||
// 判断是否需要显示签订按钮 |
|||
needSignButton() { |
|||
return this.contractData.status === 1 && ( |
|||
this.contractData.sign_file === null || |
|||
this.contractData.sign_file === '' || |
|||
this.contractData.sign_file === undefined |
|||
) |
|||
}, |
|||
|
|||
// 获取状态样式类 |
|||
getStatusClass() { |
|||
return this.needSignButton() ? 'status-unsigned' : 'status-signed' |
|||
}, |
|||
|
|||
// 获取状态文本 |
|||
getStatusText() { |
|||
return this.needSignButton() ? '待签订' : '已签订' |
|||
}, |
|||
|
|||
// 前往签名页面 |
|||
goToSign() { |
|||
uni.navigateTo({ |
|||
url: `/pages/common/contract/contract_sign?id=${this.contractId}&contractName=${encodeURIComponent(this.contractData.contract_name)}` |
|||
}) |
|||
}, |
|||
|
|||
// 预览合同模板 |
|||
previewTemplate() { |
|||
if (this.contractData.contract_template) { |
|||
const templateUrl = this.$baseUrl + '/' + this.contractData.contract_template |
|||
// #ifdef APP-PLUS |
|||
plus.runtime.openURL(templateUrl) |
|||
// #endif |
|||
|
|||
// #ifdef H5 |
|||
window.open(templateUrl, '_blank') |
|||
// #endif |
|||
|
|||
// #ifdef MP-WEIXIN |
|||
uni.downloadFile({ |
|||
url: templateUrl, |
|||
success: (res) => { |
|||
uni.openDocument({ |
|||
filePath: res.tempFilePath, |
|||
fileType: this.getFileType(this.contractData.contract_template) |
|||
}) |
|||
} |
|||
}) |
|||
// #endif |
|||
} |
|||
}, |
|||
|
|||
// 预览签名文件 |
|||
previewSignature() { |
|||
if (this.contractData.sign_file) { |
|||
this.previewImageUrl = this.getSignatureUrl() |
|||
this.showImagePreview = true |
|||
} |
|||
}, |
|||
|
|||
// 关闭图片预览 |
|||
closeImagePreview() { |
|||
this.showImagePreview = false |
|||
this.previewImageUrl = '' |
|||
}, |
|||
|
|||
// 获取签名文件URL |
|||
getSignatureUrl() { |
|||
if (this.contractData.sign_file) { |
|||
if (this.contractData.sign_file.startsWith('http')) { |
|||
return this.contractData.sign_file |
|||
} else { |
|||
return this.$baseUrl + '/' + this.contractData.sign_file |
|||
} |
|||
} |
|||
return '' |
|||
}, |
|||
|
|||
// 获取模板名称 |
|||
getTemplateName() { |
|||
if (this.contractData.contract_template) { |
|||
const parts = this.contractData.contract_template.split('/') |
|||
return parts[parts.length - 1] |
|||
} |
|||
return '合同模板' |
|||
}, |
|||
|
|||
// 获取文件类型 |
|||
getFileType(fileName) { |
|||
const ext = fileName.split('.').pop().toLowerCase() |
|||
const typeMap = { |
|||
'pdf': 'pdf', |
|||
'doc': 'doc', |
|||
'docx': 'doc', |
|||
'xls': 'xls', |
|||
'xlsx': 'xls' |
|||
} |
|||
return typeMap[ext] || 'doc' |
|||
}, |
|||
|
|||
// 下载合同 |
|||
downloadContract() { |
|||
if (this.contractData.sign_file) { |
|||
const signUrl = this.getSignatureUrl() |
|||
uni.downloadFile({ |
|||
url: signUrl, |
|||
success: (res) => { |
|||
uni.showToast({ |
|||
title: '下载成功', |
|||
icon: 'success' |
|||
}) |
|||
}, |
|||
fail: () => { |
|||
uni.showToast({ |
|||
title: '下载失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 分享合同 |
|||
shareContract() { |
|||
uni.showActionSheet({ |
|||
itemList: ['分享给好友', '保存到相册'], |
|||
success: (res) => { |
|||
if (res.tapIndex === 0) { |
|||
// 分享功能 |
|||
this.shareToFriend() |
|||
} else if (res.tapIndex === 1) { |
|||
// 保存到相册 |
|||
this.saveToAlbum() |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 分享给好友 |
|||
shareToFriend() { |
|||
uni.showToast({ |
|||
title: '分享功能开发中', |
|||
icon: 'none' |
|||
}) |
|||
}, |
|||
|
|||
// 保存到相册 |
|||
saveToAlbum() { |
|||
if (this.contractData.sign_file) { |
|||
uni.saveImageToPhotosAlbum({ |
|||
filePath: this.getSignatureUrl(), |
|||
success: () => { |
|||
uni.showToast({ |
|||
title: '保存成功', |
|||
icon: 'success' |
|||
}) |
|||
}, |
|||
fail: () => { |
|||
uni.showToast({ |
|||
title: '保存失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 格式化日期 |
|||
formatDate(dateString) { |
|||
if (!dateString) return '-' |
|||
const date = new Date(dateString) |
|||
const year = date.getFullYear() |
|||
const month = String(date.getMonth() + 1).padStart(2, '0') |
|||
const day = String(date.getDate()).padStart(2, '0') |
|||
const hour = String(date.getHours()).padStart(2, '0') |
|||
const minute = String(date.getMinutes()).padStart(2, '0') |
|||
return `${year}-${month}-${day} ${hour}:${minute}` |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.contract-detail-container { |
|||
min-height: 100vh; |
|||
background-color: #f5f5f5; |
|||
padding-bottom: 120rpx; |
|||
} |
|||
|
|||
/* 自定义导航栏 */ |
|||
.custom-nav { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
height: 88rpx; |
|||
padding: 0 32rpx; |
|||
background-color: #fff; |
|||
border-bottom: 1rpx solid #e5e5e5; |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 100; |
|||
|
|||
.nav-left, .nav-right { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.iconfont { |
|||
font-size: 36rpx; |
|||
color: #333; |
|||
} |
|||
} |
|||
|
|||
.nav-title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
|
|||
.share-btn { |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
/* 合同内容 */ |
|||
.contract-content { |
|||
padding: 20rpx; |
|||
} |
|||
|
|||
/* 状态卡片 */ |
|||
.status-card { |
|||
background: linear-gradient(135deg, #007ACC 0%, #0056b3 100%); |
|||
border-radius: 20rpx; |
|||
padding: 40rpx; |
|||
margin-bottom: 24rpx; |
|||
color: #fff; |
|||
|
|||
.status-header { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
justify-content: space-between; |
|||
margin-bottom: 24rpx; |
|||
|
|||
.status-info { |
|||
flex: 1; |
|||
|
|||
.contract-name { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
display: block; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.status-badge { |
|||
display: inline-block; |
|||
padding: 8rpx 20rpx; |
|||
border-radius: 20rpx; |
|||
font-size: 24rpx; |
|||
font-weight: 500; |
|||
background-color: rgba(255, 255, 255, 0.2); |
|||
border: 1rpx solid rgba(255, 255, 255, 0.3); |
|||
} |
|||
} |
|||
|
|||
.status-icon { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.status-icon-text { |
|||
font-size: 48rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.status-desc { |
|||
.desc-text { |
|||
font-size: 28rpx; |
|||
opacity: 0.9; |
|||
line-height: 1.5; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 信息区块 */ |
|||
.info-section, .template-section, .signature-section, .history-section { |
|||
background-color: #fff; |
|||
border-radius: 16rpx; |
|||
padding: 32rpx; |
|||
margin-bottom: 24rpx; |
|||
|
|||
.section-title { |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 24rpx; |
|||
border-left: 6rpx solid #007ACC; |
|||
padding-left: 16rpx; |
|||
} |
|||
} |
|||
|
|||
.info-list { |
|||
.info-item { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 24rpx; |
|||
|
|||
&:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.info-label { |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
width: 140rpx; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.info-value { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 模板预览 */ |
|||
.template-preview { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 24rpx; |
|||
background-color: #f8f9fa; |
|||
border-radius: 12rpx; |
|||
cursor: pointer; |
|||
|
|||
.template-icon { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background-color: #007ACC; |
|||
border-radius: 12rpx; |
|||
margin-right: 20rpx; |
|||
|
|||
.iconfont { |
|||
font-size: 32rpx; |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
.template-info { |
|||
flex: 1; |
|||
|
|||
.template-name { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
font-weight: 500; |
|||
display: block; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.template-desc { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
} |
|||
} |
|||
|
|||
.template-arrow { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.iconfont { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 签名预览 */ |
|||
.signature-preview { |
|||
position: relative; |
|||
width: 100%; |
|||
height: 300rpx; |
|||
border-radius: 12rpx; |
|||
overflow: hidden; |
|||
cursor: pointer; |
|||
|
|||
.signature-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
border-radius: 12rpx; |
|||
} |
|||
|
|||
.signature-overlay { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); |
|||
padding: 20rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.signature-text { |
|||
color: #fff; |
|||
font-size: 24rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 操作历史 */ |
|||
.history-list { |
|||
.history-item { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
margin-bottom: 32rpx; |
|||
|
|||
&:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.history-dot { |
|||
width: 20rpx; |
|||
height: 20rpx; |
|||
border-radius: 50%; |
|||
background-color: #e5e5e5; |
|||
margin-right: 20rpx; |
|||
margin-top: 8rpx; |
|||
flex-shrink: 0; |
|||
|
|||
&.active { |
|||
background-color: #007ACC; |
|||
} |
|||
} |
|||
|
|||
.history-content { |
|||
flex: 1; |
|||
|
|||
.history-title { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
font-weight: 500; |
|||
display: block; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.history-time { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 底部操作按钮 */ |
|||
.footer-actions { |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
background-color: #fff; |
|||
padding: 24rpx 32rpx; |
|||
border-top: 1rpx solid #e5e5e5; |
|||
z-index: 100; |
|||
|
|||
.action-btn { |
|||
width: 100%; |
|||
height: 88rpx; |
|||
border-radius: 12rpx; |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
border: none; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
&.primary { |
|||
background-color: #007ACC; |
|||
color: #fff; |
|||
|
|||
&:active { |
|||
background-color: #0056b3; |
|||
} |
|||
} |
|||
|
|||
&.secondary { |
|||
background-color: #f5f5f5; |
|||
color: #666; |
|||
|
|||
&:active { |
|||
background-color: #e5e5e5; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 加载状态 */ |
|||
.loading-container { |
|||
padding: 120rpx 40rpx; |
|||
text-align: center; |
|||
} |
|||
|
|||
/* 图片预览模态框 */ |
|||
.image-preview-modal { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background-color: rgba(0, 0, 0, 0.8); |
|||
z-index: 1000; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.preview-content { |
|||
position: relative; |
|||
width: 90%; |
|||
height: 70%; |
|||
|
|||
.preview-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.preview-close { |
|||
position: absolute; |
|||
top: -60rpx; |
|||
right: 0; |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.iconfont { |
|||
font-size: 36rpx; |
|||
color: #fff; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,806 @@ |
|||
<template> |
|||
<view class="contract-sign-container"> |
|||
<!-- 自定义导航栏 --> |
|||
<view class="custom-nav"> |
|||
<view class="nav-left" @click="goBack"> |
|||
<text class="iconfont icon-arrow-left"></text> |
|||
</view> |
|||
<view class="nav-title">合同签订</view> |
|||
<view class="nav-right"> |
|||
<view class="clear-btn" @click="clearSignature"> |
|||
<text class="clear-text">清除</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 合同信息 --> |
|||
<view class="contract-info"> |
|||
<view class="info-card"> |
|||
<text class="contract-name">{{contractName}}</text> |
|||
<text class="sign-tip">请在下方签名区域进行手写签名</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 签名区域 --> |
|||
<view class="signature-section"> |
|||
<view class="signature-title"> |
|||
<text class="title-text">请在此区域签名</text> |
|||
<text class="tip-text">支持手指或触控笔签名</text> |
|||
</view> |
|||
|
|||
<!-- Canvas 签名板 --> |
|||
<view class="signature-canvas-container"> |
|||
<canvas |
|||
class="signature-canvas" |
|||
canvas-id="signatureCanvas" |
|||
@touchstart="touchStart" |
|||
@touchmove="touchMove" |
|||
@touchend="touchEnd" |
|||
disable-scroll="true"> |
|||
</canvas> |
|||
|
|||
<!-- 签名提示 --> |
|||
<view v-if="!hasSigned" class="signature-placeholder"> |
|||
<text class="placeholder-icon">✍️</text> |
|||
<text class="placeholder-text">请在此处签名</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 签名工具栏 --> |
|||
<view class="signature-tools"> |
|||
<view class="tool-group"> |
|||
<text class="tool-label">笔迹颜色:</text> |
|||
<view class="color-picker"> |
|||
<view v-for="color in penColors" |
|||
:key="color" |
|||
class="color-item" |
|||
:class="currentColor === color ? 'active' : ''" |
|||
:style="{ backgroundColor: color }" |
|||
@click="setPenColor(color)"> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="tool-group"> |
|||
<text class="tool-label">笔迹粗细:</text> |
|||
<view class="width-picker"> |
|||
<view v-for="width in penWidths" |
|||
:key="width" |
|||
class="width-item" |
|||
:class="currentWidth === width ? 'active' : ''" |
|||
@click="setPenWidth(width)"> |
|||
<view class="width-preview" :style="{ width: width + 'rpx', height: width + 'rpx' }"></view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 签名预览 --> |
|||
<view v-if="signatureImageUrl" class="preview-section"> |
|||
<view class="preview-title">签名预览</view> |
|||
<view class="preview-image-container"> |
|||
<image :src="signatureImageUrl" class="preview-image" mode="aspectFit"></image> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 底部操作按钮 --> |
|||
<view class="footer-actions"> |
|||
<button class="action-btn secondary" @click="previewSignature">预览签名</button> |
|||
<button class="action-btn primary" |
|||
@click="submitSignature" |
|||
:disabled="!hasSigned || submitting" |
|||
:loading="submitting"> |
|||
{{submitting ? '提交中...' : '确认签订'}} |
|||
</button> |
|||
</view> |
|||
|
|||
<!-- 签名预览弹窗 --> |
|||
<view v-if="showPreview" class="preview-modal" @click="closePreview"> |
|||
<view class="modal-content" @click.stop> |
|||
<view class="modal-header"> |
|||
<text class="modal-title">签名预览</text> |
|||
<view class="modal-close" @click="closePreview"> |
|||
<text class="iconfont icon-close"></text> |
|||
</view> |
|||
</view> |
|||
<view class="modal-body"> |
|||
<image v-if="signatureImageUrl" :src="signatureImageUrl" class="modal-image" mode="aspectFit"></image> |
|||
<text v-else class="no-signature">暂无签名</text> |
|||
</view> |
|||
<view class="modal-footer"> |
|||
<button class="modal-btn secondary" @click="closePreview">取消</button> |
|||
<button class="modal-btn primary" @click="confirmSignature">确认签订</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import apiRoute from '@/common/axios.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
contractId: 0, |
|||
contractName: '', |
|||
canvas: null, |
|||
ctx: null, |
|||
isDrawing: false, |
|||
lastPoint: null, |
|||
hasSigned: false, |
|||
signatureImageUrl: '', |
|||
submitting: false, |
|||
showPreview: false, |
|||
|
|||
// 画笔设置 |
|||
currentColor: '#000000', |
|||
currentWidth: 6, |
|||
penColors: ['#000000', '#FF0000', '#0000FF', '#008000', '#800080'], |
|||
penWidths: [3, 6, 9, 12], |
|||
|
|||
// Canvas 尺寸 |
|||
canvasWidth: 0, |
|||
canvasHeight: 0, |
|||
pixelRatio: 1 |
|||
} |
|||
}, |
|||
onLoad(options) { |
|||
if (options.id) { |
|||
this.contractId = parseInt(options.id) |
|||
} |
|||
if (options.contractName) { |
|||
this.contractName = decodeURIComponent(options.contractName) |
|||
} |
|||
|
|||
this.$nextTick(() => { |
|||
this.initCanvas() |
|||
}) |
|||
}, |
|||
onReady() { |
|||
this.initCanvas() |
|||
}, |
|||
methods: { |
|||
// 初始化Canvas |
|||
initCanvas() { |
|||
const query = uni.createSelectorQuery().in(this) |
|||
query.select('.signature-canvas').boundingClientRect((rect) => { |
|||
if (rect) { |
|||
this.canvasWidth = rect.width |
|||
this.canvasHeight = rect.height |
|||
|
|||
// 获取设备像素比 |
|||
const systemInfo = uni.getSystemInfoSync() |
|||
this.pixelRatio = systemInfo.pixelRatio || 1 |
|||
|
|||
// 创建Canvas上下文 |
|||
this.canvas = uni.createCanvasContext('signatureCanvas', this) |
|||
this.ctx = this.canvas |
|||
|
|||
// 设置Canvas尺寸 |
|||
this.ctx.scale(this.pixelRatio, this.pixelRatio) |
|||
|
|||
// 设置画笔属性 |
|||
this.ctx.lineWidth = this.currentWidth |
|||
this.ctx.strokeStyle = this.currentColor |
|||
this.ctx.lineCap = 'round' |
|||
this.ctx.lineJoin = 'round' |
|||
|
|||
// 清空Canvas |
|||
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight) |
|||
this.ctx.draw() |
|||
} |
|||
}).exec() |
|||
}, |
|||
|
|||
// 触摸开始 |
|||
touchStart(e) { |
|||
if (!this.ctx) return |
|||
|
|||
this.isDrawing = true |
|||
const touch = e.touches[0] |
|||
this.lastPoint = { |
|||
x: touch.x, |
|||
y: touch.y |
|||
} |
|||
|
|||
this.ctx.beginPath() |
|||
this.ctx.moveTo(touch.x, touch.y) |
|||
}, |
|||
|
|||
// 触摸移动 |
|||
touchMove(e) { |
|||
if (!this.ctx || !this.isDrawing) return |
|||
|
|||
const touch = e.touches[0] |
|||
const currentPoint = { |
|||
x: touch.x, |
|||
y: touch.y |
|||
} |
|||
|
|||
this.ctx.lineTo(currentPoint.x, currentPoint.y) |
|||
this.ctx.stroke() |
|||
this.ctx.draw(true) |
|||
|
|||
this.lastPoint = currentPoint |
|||
this.hasSigned = true |
|||
}, |
|||
|
|||
// 触摸结束 |
|||
touchEnd(e) { |
|||
this.isDrawing = false |
|||
this.lastPoint = null |
|||
}, |
|||
|
|||
// 设置画笔颜色 |
|||
setPenColor(color) { |
|||
this.currentColor = color |
|||
if (this.ctx) { |
|||
this.ctx.strokeStyle = color |
|||
} |
|||
}, |
|||
|
|||
// 设置画笔粗细 |
|||
setPenWidth(width) { |
|||
this.currentWidth = width |
|||
if (this.ctx) { |
|||
this.ctx.lineWidth = width |
|||
} |
|||
}, |
|||
|
|||
// 清除签名 |
|||
clearSignature() { |
|||
if (this.ctx) { |
|||
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight) |
|||
this.ctx.draw() |
|||
this.hasSigned = false |
|||
this.signatureImageUrl = '' |
|||
} |
|||
}, |
|||
|
|||
// 预览签名 |
|||
previewSignature() { |
|||
if (!this.hasSigned) { |
|||
uni.showToast({ |
|||
title: '请先进行签名', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
this.generateSignatureImage(() => { |
|||
this.showPreview = true |
|||
}) |
|||
}, |
|||
|
|||
// 生成签名图片 |
|||
generateSignatureImage(callback) { |
|||
if (!this.canvas) return |
|||
|
|||
uni.canvasToTempFilePath({ |
|||
canvasId: 'signatureCanvas', |
|||
success: (res) => { |
|||
this.signatureImageUrl = res.tempFilePath |
|||
if (callback) callback() |
|||
}, |
|||
fail: (err) => { |
|||
console.error('生成签名图片失败:', err) |
|||
uni.showToast({ |
|||
title: '生成签名失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, this) |
|||
}, |
|||
|
|||
// 关闭预览 |
|||
closePreview() { |
|||
this.showPreview = false |
|||
}, |
|||
|
|||
// 确认签名 |
|||
confirmSignature() { |
|||
this.closePreview() |
|||
this.submitSignature() |
|||
}, |
|||
|
|||
// 提交签名 |
|||
submitSignature() { |
|||
if (!this.hasSigned) { |
|||
uni.showToast({ |
|||
title: '请先进行签名', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
this.generateSignatureImage(() => { |
|||
this.uploadSignature() |
|||
}) |
|||
}, |
|||
|
|||
// 上传签名 |
|||
async uploadSignature() { |
|||
if (!this.signatureImageUrl) { |
|||
uni.showToast({ |
|||
title: '签名生成失败,请重试', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
this.submitting = true |
|||
|
|||
try { |
|||
// 先上传签名图片 |
|||
const uploadResult = await this.uploadSignatureFile() |
|||
|
|||
if (!uploadResult.success) { |
|||
throw new Error(uploadResult.message || '上传签名文件失败') |
|||
} |
|||
|
|||
// 提交签名信息 |
|||
const response = await apiRoute.post('contract/sign', { |
|||
contract_id: this.contractId, |
|||
sign_file: uploadResult.url |
|||
}) |
|||
|
|||
if (response.data.code === 1) { |
|||
uni.showToast({ |
|||
title: '签名提交成功', |
|||
icon: 'success', |
|||
duration: 2000 |
|||
}) |
|||
|
|||
setTimeout(() => { |
|||
// 返回到合同详情页面 |
|||
uni.navigateBack({ |
|||
delta: 1 |
|||
}) |
|||
}, 2000) |
|||
} else { |
|||
throw new Error(response.data.msg || '提交签名失败') |
|||
} |
|||
} catch (error) { |
|||
console.error('提交签名失败:', error) |
|||
uni.showToast({ |
|||
title: error.message || '网络错误,请稍后重试', |
|||
icon: 'none' |
|||
}) |
|||
} finally { |
|||
this.submitting = false |
|||
} |
|||
}, |
|||
|
|||
// 上传签名文件 |
|||
uploadSignatureFile() { |
|||
return new Promise((resolve, reject) => { |
|||
uni.uploadFile({ |
|||
url: this.$baseUrl + '/uploadImage', |
|||
filePath: this.signatureImageUrl, |
|||
name: 'file', |
|||
header: { |
|||
'Authorization': uni.getStorageSync('token') |
|||
}, |
|||
success: (res) => { |
|||
try { |
|||
const data = JSON.parse(res.data) |
|||
if (data.code === 1) { |
|||
resolve({ |
|||
success: true, |
|||
url: data.data.url |
|||
}) |
|||
} else { |
|||
resolve({ |
|||
success: false, |
|||
message: data.msg || '上传失败' |
|||
}) |
|||
} |
|||
} catch (error) { |
|||
resolve({ |
|||
success: false, |
|||
message: '上传响应解析失败' |
|||
}) |
|||
} |
|||
}, |
|||
fail: (error) => { |
|||
resolve({ |
|||
success: false, |
|||
message: '上传请求失败' |
|||
}) |
|||
} |
|||
}) |
|||
}) |
|||
}, |
|||
|
|||
// 返回上一页 |
|||
goBack() { |
|||
if (this.hasSigned) { |
|||
uni.showModal({ |
|||
title: '提示', |
|||
content: '您的签名尚未保存,确定要离开吗?', |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
uni.navigateBack() |
|||
} |
|||
} |
|||
}) |
|||
} else { |
|||
uni.navigateBack() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.contract-sign-container { |
|||
min-height: 100vh; |
|||
background-color: #f5f5f5; |
|||
padding-bottom: 120rpx; |
|||
} |
|||
|
|||
/* 自定义导航栏 */ |
|||
.custom-nav { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
height: 88rpx; |
|||
padding: 0 32rpx; |
|||
background-color: #fff; |
|||
border-bottom: 1rpx solid #e5e5e5; |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 100; |
|||
|
|||
.nav-left { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.iconfont { |
|||
font-size: 36rpx; |
|||
color: #333; |
|||
} |
|||
} |
|||
|
|||
.nav-title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
|
|||
.nav-right { |
|||
width: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.clear-btn { |
|||
.clear-text { |
|||
font-size: 28rpx; |
|||
color: #007ACC; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 合同信息 */ |
|||
.contract-info { |
|||
padding: 20rpx; |
|||
|
|||
.info-card { |
|||
background-color: #fff; |
|||
border-radius: 16rpx; |
|||
padding: 32rpx; |
|||
text-align: center; |
|||
|
|||
.contract-name { |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
display: block; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.sign-tip { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 签名区域 */ |
|||
.signature-section { |
|||
padding: 0 20rpx; |
|||
margin-bottom: 40rpx; |
|||
|
|||
.signature-title { |
|||
text-align: center; |
|||
margin-bottom: 24rpx; |
|||
|
|||
.title-text { |
|||
font-size: 30rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
display: block; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.tip-text { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.signature-canvas-container { |
|||
position: relative; |
|||
background-color: #fff; |
|||
border-radius: 16rpx; |
|||
margin-bottom: 32rpx; |
|||
overflow: hidden; |
|||
border: 2rpx dashed #e5e5e5; |
|||
|
|||
.signature-canvas { |
|||
width: 100%; |
|||
height: 400rpx; |
|||
display: block; |
|||
} |
|||
|
|||
.signature-placeholder { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
text-align: center; |
|||
pointer-events: none; |
|||
|
|||
.placeholder-icon { |
|||
font-size: 60rpx; |
|||
display: block; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.placeholder-text { |
|||
font-size: 28rpx; |
|||
color: #999; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 签名工具栏 */ |
|||
.signature-tools { |
|||
background-color: #fff; |
|||
border-radius: 16rpx; |
|||
padding: 24rpx; |
|||
|
|||
.tool-group { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 24rpx; |
|||
|
|||
&:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.tool-label { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
margin-right: 20rpx; |
|||
min-width: 120rpx; |
|||
} |
|||
} |
|||
|
|||
.color-picker { |
|||
display: flex; |
|||
gap: 16rpx; |
|||
|
|||
.color-item { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
border-radius: 50%; |
|||
border: 3rpx solid transparent; |
|||
cursor: pointer; |
|||
|
|||
&.active { |
|||
border-color: #007ACC; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.width-picker { |
|||
display: flex; |
|||
gap: 20rpx; |
|||
|
|||
.width-item { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 50rpx; |
|||
height: 50rpx; |
|||
border-radius: 8rpx; |
|||
border: 2rpx solid #e5e5e5; |
|||
cursor: pointer; |
|||
|
|||
&.active { |
|||
border-color: #007ACC; |
|||
background-color: #f0f8ff; |
|||
} |
|||
|
|||
.width-preview { |
|||
background-color: #333; |
|||
border-radius: 50%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 签名预览 */ |
|||
.preview-section { |
|||
padding: 0 20rpx; |
|||
margin-bottom: 40rpx; |
|||
|
|||
.preview-title { |
|||
font-size: 30rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
text-align: center; |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.preview-image-container { |
|||
background-color: #fff; |
|||
border-radius: 16rpx; |
|||
padding: 32rpx; |
|||
|
|||
.preview-image { |
|||
width: 100%; |
|||
height: 200rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 底部操作按钮 */ |
|||
.footer-actions { |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
background-color: #fff; |
|||
padding: 24rpx 32rpx; |
|||
border-top: 1rpx solid #e5e5e5; |
|||
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-color: #007ACC; |
|||
color: #fff; |
|||
|
|||
&:active:not(:disabled) { |
|||
background-color: #0056b3; |
|||
} |
|||
|
|||
&:disabled { |
|||
background-color: #ccc; |
|||
color: #999; |
|||
} |
|||
} |
|||
|
|||
&.secondary { |
|||
background-color: #f5f5f5; |
|||
color: #666; |
|||
|
|||
&:active { |
|||
background-color: #e5e5e5; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 预览弹窗 */ |
|||
.preview-modal { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 1000; |
|||
|
|||
.modal-content { |
|||
background-color: #fff; |
|||
border-radius: 20rpx; |
|||
width: 80%; |
|||
max-height: 70%; |
|||
overflow: hidden; |
|||
|
|||
.modal-header { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 32rpx; |
|||
border-bottom: 1rpx solid #e5e5e5; |
|||
|
|||
.modal-title { |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
|
|||
.modal-close { |
|||
width: 48rpx; |
|||
height: 48rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.iconfont { |
|||
font-size: 32rpx; |
|||
color: #999; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.modal-body { |
|||
padding: 32rpx; |
|||
text-align: center; |
|||
|
|||
.modal-image { |
|||
width: 100%; |
|||
height: 300rpx; |
|||
} |
|||
|
|||
.no-signature { |
|||
font-size: 28rpx; |
|||
color: #999; |
|||
padding: 60rpx 0; |
|||
} |
|||
} |
|||
|
|||
.modal-footer { |
|||
display: flex; |
|||
gap: 24rpx; |
|||
padding: 32rpx; |
|||
border-top: 1rpx solid #e5e5e5; |
|||
|
|||
.modal-btn { |
|||
flex: 1; |
|||
height: 72rpx; |
|||
border-radius: 12rpx; |
|||
font-size: 28rpx; |
|||
font-weight: 600; |
|||
border: none; |
|||
|
|||
&.primary { |
|||
background-color: #007ACC; |
|||
color: #fff; |
|||
} |
|||
|
|||
&.secondary { |
|||
background-color: #f5f5f5; |
|||
color: #666; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,481 @@ |
|||
<template> |
|||
<view class="contract-container"> |
|||
<!-- 自定义导航栏 --> |
|||
<view class="custom-nav"> |
|||
<view class="nav-left" @click="goBack"> |
|||
<text class="iconfont icon-arrow-left"></text> |
|||
</view> |
|||
<view class="nav-title">我的合同</view> |
|||
<view class="nav-right"></view> |
|||
</view> |
|||
|
|||
<!-- 筛选栏 --> |
|||
<view class="filter-bar"> |
|||
<view class="filter-item" |
|||
:class="filterStatus === '' ? 'active' : ''" |
|||
@click="filterContract('')"> |
|||
全部 |
|||
</view> |
|||
<view class="filter-item" |
|||
:class="filterStatus === 'unsigned' ? 'active' : ''" |
|||
@click="filterContract('unsigned')"> |
|||
待签订 |
|||
</view> |
|||
<view class="filter-item" |
|||
:class="filterStatus === 'signed' ? 'active' : ''" |
|||
@click="filterContract('signed')"> |
|||
已签订 |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 合同列表 --> |
|||
<view class="contract-list"> |
|||
<view v-if="loading" class="loading-container"> |
|||
<uni-load-more status="loading" content-text="加载中..."></uni-load-more> |
|||
</view> |
|||
|
|||
<view v-else-if="filteredContracts.length === 0" class="empty-container"> |
|||
<view class="empty-icon">📄</view> |
|||
<view class="empty-text">暂无合同数据</view> |
|||
</view> |
|||
|
|||
<view v-else> |
|||
<view v-for="contract in filteredContracts" |
|||
:key="contract.id" |
|||
class="contract-item" |
|||
@click="goToDetail(contract)"> |
|||
|
|||
<!-- 合同卡片 --> |
|||
<view class="contract-card"> |
|||
<!-- 合同标题和状态 --> |
|||
<view class="contract-header"> |
|||
<view class="contract-title"> |
|||
<text class="title-text">{{contract.contract_name}}</text> |
|||
<view class="contract-status" :class="getStatusClass(contract)"> |
|||
{{getStatusText(contract)}} |
|||
</view> |
|||
</view> |
|||
<view class="contract-arrow"> |
|||
<text class="iconfont icon-arrow-right"></text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 合同信息 --> |
|||
<view class="contract-info"> |
|||
<view class="info-row"> |
|||
<text class="info-label">合同类型:</text> |
|||
<text class="info-value">{{contract.contract_type}}</text> |
|||
</view> |
|||
<view class="info-row"> |
|||
<text class="info-label">创建时间:</text> |
|||
<text class="info-value">{{formatDate(contract.created_at)}}</text> |
|||
</view> |
|||
<view class="info-row" v-if="contract.sign_time"> |
|||
<text class="info-label">签订时间:</text> |
|||
<text class="info-value">{{formatDate(contract.sign_time)}}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 操作按钮 --> |
|||
<view class="contract-actions" v-if="needSignButton(contract)"> |
|||
<button class="sign-btn" @click.stop="goToSign(contract)"> |
|||
立即签订 |
|||
</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 底部加载更多 --> |
|||
<view v-if="hasMore && !loading && filteredContracts.length > 0" class="load-more"> |
|||
<uni-load-more :status="loadMoreStatus" @clickLoadMore="loadMore"></uni-load-more> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import apiRoute from '@/common/axios.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
loading: true, |
|||
contracts: [], |
|||
filterStatus: '', // '', 'unsigned', 'signed' |
|||
currentPage: 1, |
|||
pageSize: 10, |
|||
hasMore: true, |
|||
loadMoreStatus: 'more' |
|||
} |
|||
}, |
|||
computed: { |
|||
filteredContracts() { |
|||
if (this.filterStatus === '') { |
|||
return this.contracts |
|||
} else if (this.filterStatus === 'unsigned') { |
|||
return this.contracts.filter(contract => this.needSignButton(contract)) |
|||
} else if (this.filterStatus === 'signed') { |
|||
return this.contracts.filter(contract => !this.needSignButton(contract)) |
|||
} |
|||
return this.contracts |
|||
} |
|||
}, |
|||
onLoad() { |
|||
this.loadContracts() |
|||
}, |
|||
onPullDownRefresh() { |
|||
this.refreshContracts() |
|||
}, |
|||
onReachBottom() { |
|||
if (this.hasMore && !this.loading) { |
|||
this.loadMore() |
|||
} |
|||
}, |
|||
methods: { |
|||
// 返回上一页 |
|||
goBack() { |
|||
uni.navigateBack() |
|||
}, |
|||
|
|||
// 加载合同列表 |
|||
async loadContracts(refresh = false) { |
|||
if (refresh) { |
|||
this.currentPage = 1 |
|||
this.hasMore = true |
|||
this.contracts = [] |
|||
} |
|||
|
|||
this.loading = true |
|||
this.loadMoreStatus = 'loading' |
|||
|
|||
try { |
|||
const response = await apiRoute.get('contract/myContracts', { |
|||
page: this.currentPage, |
|||
limit: this.pageSize |
|||
}) |
|||
|
|||
if (response.data.code === 1) { |
|||
const newContracts = response.data.data.data || [] |
|||
|
|||
if (refresh) { |
|||
this.contracts = newContracts |
|||
} else { |
|||
this.contracts = [...this.contracts, ...newContracts] |
|||
} |
|||
|
|||
// 检查是否还有更多数据 |
|||
this.hasMore = newContracts.length === this.pageSize |
|||
this.loadMoreStatus = this.hasMore ? 'more' : 'noMore' |
|||
} else { |
|||
uni.showToast({ |
|||
title: response.data.msg || '加载失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
} catch (error) { |
|||
console.error('加载合同列表失败:', error) |
|||
uni.showToast({ |
|||
title: '网络错误,请稍后重试', |
|||
icon: 'none' |
|||
}) |
|||
} finally { |
|||
this.loading = false |
|||
if (refresh) { |
|||
uni.stopPullDownRefresh() |
|||
} |
|||
} |
|||
}, |
|||
|
|||
// 刷新合同列表 |
|||
refreshContracts() { |
|||
this.loadContracts(true) |
|||
}, |
|||
|
|||
// 加载更多 |
|||
loadMore() { |
|||
if (this.hasMore && !this.loading) { |
|||
this.currentPage++ |
|||
this.loadContracts() |
|||
} |
|||
}, |
|||
|
|||
// 筛选合同 |
|||
filterContract(status) { |
|||
this.filterStatus = status |
|||
}, |
|||
|
|||
// 前往合同详情 |
|||
goToDetail(contract) { |
|||
uni.navigateTo({ |
|||
url: `/pages/common/contract/contract_detail?id=${contract.id}` |
|||
}) |
|||
}, |
|||
|
|||
// 前往签名页面 |
|||
goToSign(contract) { |
|||
uni.navigateTo({ |
|||
url: `/pages/common/contract/contract_sign?id=${contract.id}&contractName=${encodeURIComponent(contract.contract_name)}` |
|||
}) |
|||
}, |
|||
|
|||
// 判断是否需要显示签订按钮 |
|||
needSignButton(contract) { |
|||
return contract.status === 1 && ( |
|||
contract.sign_file === null || |
|||
contract.sign_file === '' || |
|||
contract.sign_file === undefined |
|||
) |
|||
}, |
|||
|
|||
// 获取状态样式类 |
|||
getStatusClass(contract) { |
|||
if (this.needSignButton(contract)) { |
|||
return 'status-unsigned' |
|||
} else { |
|||
return 'status-signed' |
|||
} |
|||
}, |
|||
|
|||
// 获取状态文本 |
|||
getStatusText(contract) { |
|||
if (this.needSignButton(contract)) { |
|||
return '待签订' |
|||
} else { |
|||
return '已签订' |
|||
} |
|||
}, |
|||
|
|||
// 格式化日期 |
|||
formatDate(dateString) { |
|||
if (!dateString) return '-' |
|||
const date = new Date(dateString) |
|||
const year = date.getFullYear() |
|||
const month = String(date.getMonth() + 1).padStart(2, '0') |
|||
const day = String(date.getDate()).padStart(2, '0') |
|||
return `${year}-${month}-${day}` |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.contract-container { |
|||
min-height: 100vh; |
|||
background-color: #f5f5f5; |
|||
} |
|||
|
|||
/* 自定义导航栏 */ |
|||
.custom-nav { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
height: 88rpx; |
|||
padding: 0 32rpx; |
|||
background-color: #fff; |
|||
border-bottom: 1rpx solid #e5e5e5; |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 100; |
|||
|
|||
.nav-left { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.iconfont { |
|||
font-size: 36rpx; |
|||
color: #333; |
|||
} |
|||
} |
|||
|
|||
.nav-title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
|
|||
.nav-right { |
|||
width: 80rpx; |
|||
} |
|||
} |
|||
|
|||
/* 筛选栏 */ |
|||
.filter-bar { |
|||
display: flex; |
|||
background-color: #fff; |
|||
padding: 24rpx 32rpx; |
|||
margin-bottom: 20rpx; |
|||
border-bottom: 1rpx solid #e5e5e5; |
|||
|
|||
.filter-item { |
|||
flex: 1; |
|||
text-align: center; |
|||
padding: 16rpx 24rpx; |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
border-radius: 8rpx; |
|||
transition: all 0.3s; |
|||
|
|||
&.active { |
|||
background-color: #007ACC; |
|||
color: #fff; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 合同列表 */ |
|||
.contract-list { |
|||
padding: 0 20rpx; |
|||
} |
|||
|
|||
.contract-item { |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.contract-card { |
|||
background-color: #fff; |
|||
border-radius: 16rpx; |
|||
padding: 32rpx; |
|||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); |
|||
} |
|||
|
|||
.contract-header { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
justify-content: space-between; |
|||
margin-bottom: 24rpx; |
|||
|
|||
.contract-title { |
|||
flex: 1; |
|||
|
|||
.title-text { |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
display: block; |
|||
margin-bottom: 12rpx; |
|||
} |
|||
|
|||
.contract-status { |
|||
display: inline-block; |
|||
padding: 8rpx 16rpx; |
|||
border-radius: 20rpx; |
|||
font-size: 24rpx; |
|||
font-weight: 500; |
|||
|
|||
&.status-unsigned { |
|||
background-color: #fff3cd; |
|||
color: #856404; |
|||
border: 1rpx solid #ffeaa7; |
|||
} |
|||
|
|||
&.status-signed { |
|||
background-color: #d4edda; |
|||
color: #155724; |
|||
border: 1rpx solid #c3e6cb; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.contract-arrow { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.iconfont { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.contract-info { |
|||
.info-row { |
|||
display: flex; |
|||
margin-bottom: 16rpx; |
|||
|
|||
.info-label { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
width: 140rpx; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.info-value { |
|||
font-size: 26rpx; |
|||
color: #333; |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.contract-actions { |
|||
margin-top: 24rpx; |
|||
padding-top: 24rpx; |
|||
border-top: 1rpx solid #f0f0f0; |
|||
|
|||
.sign-btn { |
|||
width: 100%; |
|||
height: 72rpx; |
|||
background-color: #007ACC; |
|||
color: #fff; |
|||
border: none; |
|||
border-radius: 12rpx; |
|||
font-size: 28rpx; |
|||
font-weight: 600; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
&:active { |
|||
background-color: #0056b3; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 加载状态 */ |
|||
.loading-container { |
|||
padding: 60rpx 0; |
|||
text-align: center; |
|||
} |
|||
|
|||
.empty-container { |
|||
text-align: center; |
|||
padding: 120rpx 40rpx; |
|||
|
|||
.empty-icon { |
|||
font-size: 120rpx; |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.empty-text { |
|||
font-size: 28rpx; |
|||
color: #999; |
|||
} |
|||
} |
|||
|
|||
.load-more { |
|||
padding: 40rpx 0; |
|||
} |
|||
|
|||
/* 动画效果 */ |
|||
.contract-item { |
|||
animation: slideIn 0.3s ease-out; |
|||
} |
|||
|
|||
@keyframes slideIn { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(20rpx); |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,901 @@ |
|||
<template> |
|||
<view class="personnel-form-container"> |
|||
<!-- 自定义导航栏 --> |
|||
<view class="custom-nav"> |
|||
<view class="nav-left" @click="goBack"> |
|||
<text class="iconfont icon-arrow-left"></text> |
|||
</view> |
|||
<view class="nav-title">新员工信息填写</view> |
|||
<view class="nav-right"></view> |
|||
</view> |
|||
|
|||
<!-- 进度条 --> |
|||
<view class="progress-container"> |
|||
<view class="progress-bar"> |
|||
<view class="progress-step" :class="currentStep >= 1 ? 'active' : ''"> |
|||
<text class="step-number">1</text> |
|||
<text class="step-text">基本信息</text> |
|||
</view> |
|||
<view class="progress-line" :class="currentStep >= 2 ? 'active' : ''"></view> |
|||
<view class="progress-step" :class="currentStep >= 2 ? 'active' : ''"> |
|||
<text class="step-number">2</text> |
|||
<text class="step-text">详细信息</text> |
|||
</view> |
|||
<view class="progress-line" :class="currentStep >= 3 ? 'active' : ''"></view> |
|||
<view class="progress-step" :class="currentStep >= 3 ? 'active' : ''"> |
|||
<text class="step-number">3</text> |
|||
<text class="step-text">确认提交</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 表单内容 --> |
|||
<view class="form-content"> |
|||
<!-- 第一步:基本信息 --> |
|||
<view v-if="currentStep === 1" class="step-content"> |
|||
<view class="section-title">基本信息</view> |
|||
|
|||
<!-- 头像上传 --> |
|||
<view class="form-item"> |
|||
<view class="label">头像</view> |
|||
<view class="avatar-upload" @click="chooseAvatar"> |
|||
<image v-if="formData.head_img" :src="formData.head_img" class="avatar-preview"></image> |
|||
<view v-else class="avatar-placeholder"> |
|||
<text class="iconfont icon-camera"></text> |
|||
<text class="placeholder-text">点击上传头像</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 姓名 --> |
|||
<view class="form-item required"> |
|||
<view class="label">姓名</view> |
|||
<input class="form-input" v-model="formData.name" placeholder="请输入姓名" /> |
|||
</view> |
|||
|
|||
<!-- 性别 --> |
|||
<view class="form-item required"> |
|||
<view class="label">性别</view> |
|||
<view class="radio-group"> |
|||
<label class="radio-item" @click="formData.gender = 1"> |
|||
<view class="radio" :class="formData.gender === 1 ? 'checked' : ''"></view> |
|||
<text>男</text> |
|||
</label> |
|||
<label class="radio-item" @click="formData.gender = 0"> |
|||
<view class="radio" :class="formData.gender === 0 ? 'checked' : ''"></view> |
|||
<text>女</text> |
|||
</label> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 生日 --> |
|||
<view class="form-item"> |
|||
<view class="label">出生日期</view> |
|||
<picker mode="date" :value="formData.birthday" @change="onBirthdayChange"> |
|||
<view class="picker-input"> |
|||
{{formData.birthday || '请选择出生日期'}} |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
|
|||
<!-- 手机号 --> |
|||
<view class="form-item required"> |
|||
<view class="label">手机号码</view> |
|||
<input class="form-input" v-model="formData.phone" placeholder="请输入手机号码" type="number" /> |
|||
</view> |
|||
|
|||
<!-- 邮箱 --> |
|||
<view class="form-item"> |
|||
<view class="label">邮箱</view> |
|||
<input class="form-input" v-model="formData.email" placeholder="请输入邮箱地址" /> |
|||
</view> |
|||
|
|||
<!-- 微信号 --> |
|||
<view class="form-item"> |
|||
<view class="label">微信号</view> |
|||
<input class="form-input" v-model="formData.wx" placeholder="请输入微信号" /> |
|||
</view> |
|||
|
|||
<!-- 账号类型 --> |
|||
<view class="form-item required"> |
|||
<view class="label">职位类型</view> |
|||
<view class="radio-group"> |
|||
<label class="radio-item" @click="formData.account_type = 'teacher'"> |
|||
<view class="radio" :class="formData.account_type === 'teacher' ? 'checked' : ''"></view> |
|||
<text>教师</text> |
|||
</label> |
|||
<label class="radio-item" @click="formData.account_type = 'market'"> |
|||
<view class="radio" :class="formData.account_type === 'market' ? 'checked' : ''"></view> |
|||
<text>市场</text> |
|||
</label> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 入职时间 --> |
|||
<view class="form-item"> |
|||
<view class="label">入职时间</view> |
|||
<picker mode="date" :value="formData.join_time" @change="onJoinTimeChange"> |
|||
<view class="picker-input"> |
|||
{{formData.join_time || '请选择入职时间'}} |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 第二步:详细信息 --> |
|||
<view v-if="currentStep === 2" class="step-content"> |
|||
<view class="section-title">详细信息</view> |
|||
|
|||
<!-- 民族 --> |
|||
<view class="form-item"> |
|||
<view class="label">民族</view> |
|||
<input class="form-input" v-model="detailData.ethnicity" placeholder="请输入民族" /> |
|||
</view> |
|||
|
|||
<!-- 年龄 --> |
|||
<view class="form-item"> |
|||
<view class="label">年龄</view> |
|||
<input class="form-input" v-model.number="detailData.age" placeholder="请输入年龄" type="number" /> |
|||
</view> |
|||
|
|||
<!-- 政治面貌 --> |
|||
<view class="form-item"> |
|||
<view class="label">政治面貌</view> |
|||
<picker :range="politicsOptions" @change="onPoliticsChange"> |
|||
<view class="picker-input"> |
|||
{{detailData.politics || '请选择政治面貌'}} |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
|
|||
<!-- 毕业院校 --> |
|||
<view class="form-item"> |
|||
<view class="label">毕业院校</view> |
|||
<input class="form-input" v-model="detailData.university" placeholder="请输入毕业院校" /> |
|||
</view> |
|||
|
|||
<!-- 学历 --> |
|||
<view class="form-item"> |
|||
<view class="label">学历</view> |
|||
<picker :range="educationOptions" @change="onEducationChange"> |
|||
<view class="picker-input"> |
|||
{{detailData.education || '请选择学历'}} |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
|
|||
<!-- 专业 --> |
|||
<view class="form-item"> |
|||
<view class="label">专业</view> |
|||
<input class="form-input" v-model="detailData.major" placeholder="请输入专业" /> |
|||
</view> |
|||
|
|||
<!-- 毕业日期 --> |
|||
<view class="form-item"> |
|||
<view class="label">毕业日期</view> |
|||
<picker mode="date" :value="detailData.graduation_date" @change="onGraduationDateChange"> |
|||
<view class="picker-input"> |
|||
{{detailData.graduation_date || '请选择毕业日期'}} |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
|
|||
<!-- 籍贯 --> |
|||
<view class="form-item"> |
|||
<view class="label">籍贯</view> |
|||
<input class="form-input" v-model="detailData.native_place" placeholder="请输入籍贯" /> |
|||
</view> |
|||
|
|||
<!-- 户口所在地 --> |
|||
<view class="form-item"> |
|||
<view class="label">户口所在地</view> |
|||
<input class="form-input" v-model="detailData.household_place" placeholder="请输入户口所在地" /> |
|||
</view> |
|||
|
|||
<!-- 户口性质 --> |
|||
<view class="form-item"> |
|||
<view class="label">户口性质</view> |
|||
<picker :range="householdTypeOptions" @change="onHouseholdTypeChange"> |
|||
<view class="picker-input"> |
|||
{{detailData.household_type || '请选择户口性质'}} |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
|
|||
<!-- 户籍地址 --> |
|||
<view class="form-item"> |
|||
<view class="label">户籍地址</view> |
|||
<textarea class="form-textarea" v-model="detailData.household_address" placeholder="请输入户籍地址"></textarea> |
|||
</view> |
|||
|
|||
<!-- 现居地址 --> |
|||
<view class="form-item"> |
|||
<view class="label">现居地址</view> |
|||
<textarea class="form-textarea" v-model="detailData.current_address" placeholder="请输入现居地址"></textarea> |
|||
</view> |
|||
|
|||
<!-- 紧急联系人 --> |
|||
<view class="form-item"> |
|||
<view class="label">紧急联系人</view> |
|||
<input class="form-input" v-model="detailData.emergency_contact" placeholder="请输入紧急联系人姓名" /> |
|||
</view> |
|||
|
|||
<!-- 紧急联系电话 --> |
|||
<view class="form-item"> |
|||
<view class="label">紧急联系电话</view> |
|||
<input class="form-input" v-model="detailData.emergency_phone" placeholder="请输入紧急联系电话" type="number" /> |
|||
</view> |
|||
|
|||
<!-- 婚姻状况 --> |
|||
<view class="form-item"> |
|||
<view class="label">婚姻状况</view> |
|||
<picker :range="maritalStatusOptions" @change="onMaritalStatusChange"> |
|||
<view class="picker-input"> |
|||
{{detailData.marital_status || '请选择婚姻状况'}} |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
|
|||
<!-- 银行卡号 --> |
|||
<view class="form-item"> |
|||
<view class="label">银行卡号</view> |
|||
<input class="form-input" v-model="detailData.bank_card" placeholder="请输入银行卡号" type="number" /> |
|||
</view> |
|||
|
|||
<!-- 开户银行 --> |
|||
<view class="form-item"> |
|||
<view class="label">开户银行</view> |
|||
<input class="form-input" v-model="detailData.bank_name" placeholder="请输入开户银行" /> |
|||
</view> |
|||
|
|||
<!-- 备注 --> |
|||
<view class="form-item"> |
|||
<view class="label">备注</view> |
|||
<textarea class="form-textarea" v-model="detailData.remark" placeholder="请输入备注信息"></textarea> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 第三步:确认信息 --> |
|||
<view v-if="currentStep === 3" class="step-content"> |
|||
<view class="section-title">确认信息</view> |
|||
<view class="confirm-info"> |
|||
<view class="info-section"> |
|||
<view class="info-title">基本信息</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">姓名:</text> |
|||
<text class="info-value">{{formData.name}}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">性别:</text> |
|||
<text class="info-value">{{formData.gender === 1 ? '男' : '女'}}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">手机:</text> |
|||
<text class="info-value">{{formData.phone}}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">职位:</text> |
|||
<text class="info-value">{{formData.account_type === 'teacher' ? '教师' : '市场'}}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="info-section"> |
|||
<view class="info-title">详细信息</view> |
|||
<view class="info-item" v-if="detailData.education"> |
|||
<text class="info-label">学历:</text> |
|||
<text class="info-value">{{detailData.education}}</text> |
|||
</view> |
|||
<view class="info-item" v-if="detailData.university"> |
|||
<text class="info-label">毕业院校:</text> |
|||
<text class="info-value">{{detailData.university}}</text> |
|||
</view> |
|||
<view class="info-item" v-if="detailData.emergency_contact"> |
|||
<text class="info-label">紧急联系人:</text> |
|||
<text class="info-value">{{detailData.emergency_contact}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 底部按钮 --> |
|||
<view class="footer-buttons"> |
|||
<button v-if="currentStep > 1" class="btn btn-secondary" @click="prevStep">上一步</button> |
|||
<button v-if="currentStep < 3" class="btn btn-primary" @click="nextStep">下一步</button> |
|||
<button v-if="currentStep === 3" class="btn btn-primary" @click="submitForm" :loading="submitting">提交</button> |
|||
</view> |
|||
|
|||
<!-- 加载提示 --> |
|||
<uni-load-more v-if="submitting" status="loading" content-text="正在提交..."></uni-load-more> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import apiRoute from '@/common/axios.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
currentStep: 1, |
|||
submitting: false, |
|||
// 基本信息 |
|||
formData: { |
|||
name: '', |
|||
head_img: '', |
|||
gender: null, |
|||
birthday: '', |
|||
phone: '', |
|||
email: '', |
|||
wx: '', |
|||
account_type: '', |
|||
join_time: '' |
|||
}, |
|||
// 详细信息 |
|||
detailData: { |
|||
ethnicity: '', |
|||
age: null, |
|||
politics: '', |
|||
university: '', |
|||
education: '', |
|||
major: '', |
|||
graduation_date: '', |
|||
native_place: '', |
|||
household_place: '', |
|||
household_type: '', |
|||
household_address: '', |
|||
current_address: '', |
|||
emergency_contact: '', |
|||
emergency_phone: '', |
|||
marital_status: '', |
|||
bank_card: '', |
|||
bank_name: '', |
|||
remark: '' |
|||
}, |
|||
// 选项数据 |
|||
politicsOptions: ['群众', '共青团员', '中共党员', '民主党派', '无党派人士'], |
|||
educationOptions: ['高中', '中专', '大专', '本科', '硕士', '博士'], |
|||
householdTypeOptions: ['城镇', '农村'], |
|||
maritalStatusOptions: ['未婚', '已婚', '离异', '丧偶'] |
|||
} |
|||
}, |
|||
onLoad() { |
|||
// 设置当前日期为默认入职时间 |
|||
const today = new Date() |
|||
const year = today.getFullYear() |
|||
const month = String(today.getMonth() + 1).padStart(2, '0') |
|||
const day = String(today.getDate()).padStart(2, '0') |
|||
this.formData.join_time = `${year}-${month}-${day}` |
|||
}, |
|||
methods: { |
|||
// 返回上一页 |
|||
goBack() { |
|||
uni.navigateBack() |
|||
}, |
|||
|
|||
// 选择头像 |
|||
chooseAvatar() { |
|||
uni.chooseImage({ |
|||
count: 1, |
|||
sizeType: ['compressed'], |
|||
sourceType: ['album', 'camera'], |
|||
success: (res) => { |
|||
const tempFilePath = res.tempFilePaths[0] |
|||
this.uploadAvatar(tempFilePath) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 上传头像 |
|||
uploadAvatar(filePath) { |
|||
uni.showLoading({ title: '上传中...' }) |
|||
|
|||
uni.uploadFile({ |
|||
url: this.$baseUrl + '/uploadImage', |
|||
filePath: filePath, |
|||
name: 'file', |
|||
header: { |
|||
'Authorization': uni.getStorageSync('token') |
|||
}, |
|||
success: (res) => { |
|||
const data = JSON.parse(res.data) |
|||
if (data.code === 1) { |
|||
this.formData.head_img = data.data.url |
|||
uni.showToast({ title: '头像上传成功', icon: 'success' }) |
|||
} else { |
|||
uni.showToast({ title: data.msg || '上传失败', icon: 'none' }) |
|||
} |
|||
}, |
|||
fail: () => { |
|||
uni.showToast({ title: '上传失败', icon: 'none' }) |
|||
}, |
|||
complete: () => { |
|||
uni.hideLoading() |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 日期选择事件 |
|||
onBirthdayChange(e) { |
|||
this.formData.birthday = e.detail.value |
|||
// 自动计算年龄 |
|||
this.calculateAge() |
|||
}, |
|||
|
|||
onJoinTimeChange(e) { |
|||
this.formData.join_time = e.detail.value |
|||
}, |
|||
|
|||
onGraduationDateChange(e) { |
|||
this.detailData.graduation_date = e.detail.value |
|||
}, |
|||
|
|||
// 下拉选择事件 |
|||
onPoliticsChange(e) { |
|||
this.detailData.politics = this.politicsOptions[e.detail.value] |
|||
}, |
|||
|
|||
onEducationChange(e) { |
|||
this.detailData.education = this.educationOptions[e.detail.value] |
|||
}, |
|||
|
|||
onHouseholdTypeChange(e) { |
|||
this.detailData.household_type = this.householdTypeOptions[e.detail.value] |
|||
}, |
|||
|
|||
onMaritalStatusChange(e) { |
|||
this.detailData.marital_status = this.maritalStatusOptions[e.detail.value] |
|||
}, |
|||
|
|||
// 计算年龄 |
|||
calculateAge() { |
|||
if (this.formData.birthday) { |
|||
const birthDate = new Date(this.formData.birthday) |
|||
const today = new Date() |
|||
let age = today.getFullYear() - birthDate.getFullYear() |
|||
const monthDiff = today.getMonth() - birthDate.getMonth() |
|||
|
|||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { |
|||
age-- |
|||
} |
|||
|
|||
this.detailData.age = age |
|||
} |
|||
}, |
|||
|
|||
// 步骤控制 |
|||
nextStep() { |
|||
if (this.validateCurrentStep()) { |
|||
this.currentStep++ |
|||
} |
|||
}, |
|||
|
|||
prevStep() { |
|||
this.currentStep-- |
|||
}, |
|||
|
|||
// 验证当前步骤 |
|||
validateCurrentStep() { |
|||
if (this.currentStep === 1) { |
|||
if (!this.formData.name) { |
|||
uni.showToast({ title: '请输入姓名', icon: 'none' }) |
|||
return false |
|||
} |
|||
if (this.formData.gender === null) { |
|||
uni.showToast({ title: '请选择性别', icon: 'none' }) |
|||
return false |
|||
} |
|||
if (!this.formData.phone) { |
|||
uni.showToast({ title: '请输入手机号码', icon: 'none' }) |
|||
return false |
|||
} |
|||
if (!/^1[3-9]\d{9}$/.test(this.formData.phone)) { |
|||
uni.showToast({ title: '请输入正确的手机号码', icon: 'none' }) |
|||
return false |
|||
} |
|||
if (!this.formData.account_type) { |
|||
uni.showToast({ title: '请选择职位类型', icon: 'none' }) |
|||
return false |
|||
} |
|||
} |
|||
return true |
|||
}, |
|||
|
|||
// 提交表单 |
|||
async submitForm() { |
|||
if (!this.validateCurrentStep()) { |
|||
return |
|||
} |
|||
|
|||
this.submitting = true |
|||
|
|||
try { |
|||
const submitData = { |
|||
...this.formData, |
|||
...this.detailData |
|||
} |
|||
|
|||
const response = await apiRoute.post('personnel/add', submitData) |
|||
|
|||
if (response.data.code === 1) { |
|||
uni.showToast({ |
|||
title: '员工信息提交成功', |
|||
icon: 'success', |
|||
duration: 2000 |
|||
}) |
|||
|
|||
setTimeout(() => { |
|||
uni.navigateBack() |
|||
}, 2000) |
|||
} else { |
|||
uni.showToast({ |
|||
title: response.data.msg || '提交失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
} catch (error) { |
|||
console.error('提交员工信息失败:', error) |
|||
uni.showToast({ |
|||
title: '网络错误,请稍后重试', |
|||
icon: 'none' |
|||
}) |
|||
} finally { |
|||
this.submitting = false |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.personnel-form-container { |
|||
min-height: 100vh; |
|||
background-color: #f5f5f5; |
|||
padding-bottom: 120rpx; |
|||
} |
|||
|
|||
/* 自定义导航栏 */ |
|||
.custom-nav { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
height: 88rpx; |
|||
padding: 0 32rpx; |
|||
background-color: #fff; |
|||
border-bottom: 1rpx solid #e5e5e5; |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 100; |
|||
|
|||
.nav-left { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.iconfont { |
|||
font-size: 36rpx; |
|||
color: #333; |
|||
} |
|||
} |
|||
|
|||
.nav-title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
|
|||
.nav-right { |
|||
width: 80rpx; |
|||
} |
|||
} |
|||
|
|||
/* 进度条 */ |
|||
.progress-container { |
|||
background-color: #fff; |
|||
padding: 40rpx 32rpx; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.progress-bar { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.progress-step { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
flex: 1; |
|||
|
|||
.step-number { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
border-radius: 50%; |
|||
background-color: #e5e5e5; |
|||
color: #999; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 28rpx; |
|||
font-weight: 600; |
|||
margin-bottom: 12rpx; |
|||
} |
|||
|
|||
.step-text { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
&.active { |
|||
.step-number { |
|||
background-color: #007ACC; |
|||
color: #fff; |
|||
} |
|||
|
|||
.step-text { |
|||
color: #007ACC; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.progress-line { |
|||
flex: 1; |
|||
height: 4rpx; |
|||
background-color: #e5e5e5; |
|||
margin: 0 20rpx; |
|||
margin-top: -30rpx; |
|||
|
|||
&.active { |
|||
background-color: #007ACC; |
|||
} |
|||
} |
|||
|
|||
/* 表单内容 */ |
|||
.form-content { |
|||
background-color: #fff; |
|||
margin: 0 20rpx 20rpx; |
|||
border-radius: 16rpx; |
|||
padding: 32rpx; |
|||
} |
|||
|
|||
.section-title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 40rpx; |
|||
border-left: 8rpx solid #007ACC; |
|||
padding-left: 20rpx; |
|||
} |
|||
|
|||
.form-item { |
|||
margin-bottom: 40rpx; |
|||
|
|||
&.required .label::before { |
|||
content: '*'; |
|||
color: #ff4d4f; |
|||
margin-right: 8rpx; |
|||
} |
|||
|
|||
.label { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
margin-bottom: 16rpx; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.form-input, |
|||
.picker-input { |
|||
width: 100%; |
|||
height: 88rpx; |
|||
border: 2rpx solid #e5e5e5; |
|||
border-radius: 12rpx; |
|||
padding: 0 24rpx; |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
background-color: #fff; |
|||
box-sizing: border-box; |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
&:focus { |
|||
border-color: #007ACC; |
|||
} |
|||
} |
|||
|
|||
.form-textarea { |
|||
width: 100%; |
|||
min-height: 120rpx; |
|||
border: 2rpx solid #e5e5e5; |
|||
border-radius: 12rpx; |
|||
padding: 20rpx 24rpx; |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
background-color: #fff; |
|||
box-sizing: border-box; |
|||
resize: none; |
|||
|
|||
&:focus { |
|||
border-color: #007ACC; |
|||
} |
|||
} |
|||
|
|||
.picker-input { |
|||
color: #999; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
/* 头像上传 */ |
|||
.avatar-upload { |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
border: 2rpx solid #e5e5e5; |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
overflow: hidden; |
|||
cursor: pointer; |
|||
|
|||
.avatar-preview { |
|||
width: 100%; |
|||
height: 100%; |
|||
border-radius: 50%; |
|||
} |
|||
|
|||
.avatar-placeholder { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
color: #999; |
|||
|
|||
.iconfont { |
|||
font-size: 32rpx; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.placeholder-text { |
|||
font-size: 20rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 单选按钮组 */ |
|||
.radio-group { |
|||
display: flex; |
|||
gap: 60rpx; |
|||
} |
|||
|
|||
.radio-item { |
|||
display: flex; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
|
|||
.radio { |
|||
width: 32rpx; |
|||
height: 32rpx; |
|||
border: 2rpx solid #e5e5e5; |
|||
border-radius: 50%; |
|||
margin-right: 16rpx; |
|||
position: relative; |
|||
|
|||
&.checked { |
|||
border-color: #007ACC; |
|||
|
|||
&::after { |
|||
content: ''; |
|||
width: 16rpx; |
|||
height: 16rpx; |
|||
background-color: #007ACC; |
|||
border-radius: 50%; |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
} |
|||
} |
|||
} |
|||
|
|||
text { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
} |
|||
} |
|||
|
|||
/* 确认信息 */ |
|||
.confirm-info { |
|||
.info-section { |
|||
margin-bottom: 40rpx; |
|||
|
|||
.info-title { |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 24rpx; |
|||
border-left: 6rpx solid #007ACC; |
|||
padding-left: 16rpx; |
|||
} |
|||
|
|||
.info-item { |
|||
display: flex; |
|||
margin-bottom: 16rpx; |
|||
|
|||
.info-label { |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
width: 120rpx; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.info-value { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 底部按钮 */ |
|||
.footer-buttons { |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
background-color: #fff; |
|||
padding: 24rpx 32rpx 24rpx; |
|||
border-top: 1rpx solid #e5e5e5; |
|||
display: flex; |
|||
gap: 24rpx; |
|||
z-index: 100; |
|||
|
|||
.btn { |
|||
flex: 1; |
|||
height: 88rpx; |
|||
border-radius: 12rpx; |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
border: none; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
&.btn-primary { |
|||
background-color: #007ACC; |
|||
color: #fff; |
|||
|
|||
&:active { |
|||
background-color: #0056b3; |
|||
} |
|||
} |
|||
|
|||
&.btn-secondary { |
|||
background-color: #f5f5f5; |
|||
color: #666; |
|||
|
|||
&:active { |
|||
background-color: #e5e5e5; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 步骤内容动画 */ |
|||
.step-content { |
|||
animation: fadeIn 0.3s ease-in-out; |
|||
} |
|||
|
|||
@keyframes fadeIn { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateX(20rpx); |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateX(0); |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue