This commit is contained in:
BianLzhaoMin 2025-12-16 16:45:26 +08:00
parent 14208fbfd2
commit 46a34253ab
9 changed files with 888 additions and 104 deletions

View File

@ -2,10 +2,10 @@
<view class="signature-container">
<view class="toolbar">
<up-button text="清除" type="default" size="small" @tap="handleClear" />
<up-button text="保存" type="primary" size="small" @tap="handleSave" />
<!-- <up-button text="保存" type="primary" size="small" @tap="handleSave" /> -->
</view>
<view> 签名区域 </view>
<view class="title"> 签名区域 </view>
<canvas
class="signature-canvas"
:canvas-id="canvasId"
@ -19,7 +19,8 @@
@mousemove="handleMove"
@mouseup="handleEnd"
@mouseleave="handleEnd"
></canvas>
>
</canvas>
</view>
</template>
@ -33,7 +34,7 @@ const props = defineProps({
},
height: {
type: Number,
default: 240,
default: 280,
},
lineWidth: {
type: Number,
@ -50,14 +51,21 @@ const emit = defineEmits(['save', 'clear'])
const ctx = ref(null)
const isDrawing = ref(false)
const lastPoint = ref({ x: 0, y: 0 })
//
const hasDrawn = ref(false)
onMounted(() => {
const instance = getCurrentInstance()
ctx.value = uni.createCanvasContext(props.canvasId, instance)
const setupCtx = () => {
if (!ctx.value) return
ctx.value.setStrokeStyle(props.strokeColor)
ctx.value.setLineWidth(props.lineWidth)
ctx.value.setLineCap('round')
ctx.value.setLineJoin('round')
}
onMounted(() => {
const instance = getCurrentInstance()
ctx.value = uni.createCanvasContext(props.canvasId, instance)
setupCtx()
ctx.value.draw()
})
@ -94,6 +102,8 @@ const handleMove = (e) => {
ctx.value.stroke()
ctx.value.draw(true)
//
hasDrawn.value = true
lastPoint.value = p
}
@ -114,7 +124,11 @@ const getPoint = (e) => {
const handleClear = () => {
ctx.value.clearRect(0, 0, 1000, 600)
ctx.value.draw()
// draw(false)
ctx.value.draw(false, () => {
setupCtx()
})
hasDrawn.value = false
emit('clear')
}
@ -134,6 +148,33 @@ const handleSave = () => {
instance,
)
}
const exportSignature = () => {
//
if (!hasDrawn.value) {
uni.$u.toast('请先完成签名')
return Promise.reject(new Error('EMPTY_SIGNATURE'))
}
const instance = getCurrentInstance()
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath(
{
canvasId: props.canvasId,
success: (res) => resolve(res.tempFilePath),
fail: (err) => {
uni.$u.toast('导出签名失败')
reject(err)
},
},
instance,
)
})
}
defineExpose({
exportSignature,
})
</script>
<style lang="scss" scoped>
@ -151,6 +192,14 @@ const handleSave = () => {
background: #f5f5f5;
}
.title {
font-size: 28rpx;
color: #333;
padding: 12rpx 0;
text-align: center;
border-bottom: 1rpx solid #e5e5e5;
}
.signature-canvas {
width: 100%;
background: #fff;

View File

@ -61,7 +61,11 @@
:borderBottom="true"
required
>
<up-input v-model="form.partyA" placeholder="请输入甲方公司名称" />
<up-input
disabled
v-model="form.partyA"
placeholder="甲方公司名称"
/>
</up-form-item>
<up-form-item
label="地址"
@ -69,7 +73,7 @@
:borderBottom="true"
required
>
<up-input v-model="form.partAAdress" placeholder="请输入地址" />
<up-input disabled v-model="form.partAAdress" placeholder="地址" />
</up-form-item>
<up-form-item
label="法定代表人"
@ -78,17 +82,17 @@
required
>
<up-input
disabled
v-model="form.legalPerson"
placeholder="请输入法定代表人"
placeholder="法定代表人"
/>
</up-form-item>
<up-form-item
label="联系电话"
prop="partAPhone"
:borderBottom="true"
required
>
<up-input v-model="form.partAPhone" placeholder="请输入联系电话" />
<up-form-item label="联系电话" :borderBottom="true">
<up-input
disabled
v-model="form.partAPhone"
placeholder="联系电话"
/>
</up-form-item>
<up-form-item
label="乙方(劳动者姓名)"
@ -96,7 +100,7 @@
:borderBottom="true"
required
>
<up-input v-model="form.partyB" placeholder="请输入劳动者姓名" />
<up-input disabled v-model="form.partyB" placeholder="劳动者姓名" />
</up-form-item>
<up-form-item
label="身份证号"
@ -104,7 +108,11 @@
:borderBottom="true"
required
>
<up-input v-model="form.partBIdCard" placeholder="请输入身份证号" />
<up-input
disabled
v-model="form.partBIdCard"
placeholder="身份证号"
/>
</up-form-item>
<up-form-item
label="乙方电话"
@ -112,7 +120,11 @@
:borderBottom="true"
required
>
<up-input v-model="form.partBPhone" placeholder="请输入联系方式" />
<up-input
disabled
v-model="form.partBPhone"
placeholder="乙方电话"
/>
</up-form-item>
<up-form-item
label="联系住址"
@ -120,7 +132,11 @@
:borderBottom="true"
required
>
<up-input v-model="form.partBAdress" placeholder="请输入联系住址" />
<up-input
disabled
v-model="form.partBAdress"
placeholder="联系住址"
/>
</up-form-item>
<up-form-item
label="合同生效日期"
@ -143,10 +159,29 @@
prop="workTask"
:borderBottom="true"
>
<up-input v-model="form.workTask" placeholder="请输入工作岗位" />
<up-input disabled v-model="form.workTask" placeholder="工作岗位" />
</up-form-item>
<up-form-item label="工作地点" prop="workAdress" :borderBottom="true">
<up-input v-model="form.workAdress" placeholder="请输入工作地点" />
<up-input
disabled
v-model="form.workAdress"
placeholder="工作地点"
/>
</up-form-item>
<up-form-item
label="是否为小包干班组"
prop="isXbg"
:borderBottom="true"
>
<view style="width: 100%">
<CommonPicker
v-model="form.isXbg"
:options="xbgOptions"
@change="handleSelectXbg"
placeholder="请选择是否为小包干班组"
/>
</view>
</up-form-item>
</view>
@ -171,6 +206,7 @@
<up-form-item
label="核定标准(元)"
prop="verStand"
required
:borderBottom="true"
>
<up-input v-model="form.verStand" placeholder="请输入核定标准" />
@ -183,6 +219,7 @@
label="绩效核定方式"
prop="achievementsVerification"
:borderBottom="true"
required
>
<view style="width: 100%">
<CommonPicker
@ -198,6 +235,7 @@
label="绩效奖金区间(元) 起"
prop="bonusA"
class="half"
required
:borderBottom="true"
>
<up-input v-model="form.bonusA" placeholder="起" />
@ -207,6 +245,7 @@
label="绩效奖金区间(元) 止"
prop="bonusB"
class="half"
required
:borderBottom="true"
>
<up-input v-model="form.bonusB" placeholder="止" />
@ -268,26 +307,47 @@
<view class="section">
<view class="section-title">验证码</view>
<view class="captcha-row">
<up-input v-model="form.captcha" placeholder="请输入验证码" />
<up-button
text="获取验证码"
type="primary"
size="small"
:customStyle="{ width: '180rpx', marginLeft: '12rpx' }"
@tap="handleGetCaptcha"
/>
<!-- <up-input v-model="form.captcha" placeholder="请输入验证码" /> -->
<up-code-input
v-model="form.captcha"
maxlength="4"
space="30"
></up-code-input>
<view>
<up-button
:text="
captchaCountdown > 0
? `${captchaCountdown}s后重试`
: '获取验证码'
"
type="primary"
size="small"
:disabled="captchaCountdown > 0"
:customStyle="{ width: '180rpx', marginLeft: '12rpx' }"
@tap="handleGetCaptcha"
/>
</view>
</view>
</view>
<!-- 附件列表 -->
<view class="section">
<view class="section-title">附件预览</view>
<view v-for="item in attachments" :key="item.title" class="attach-row">
<view class="attach-title">{{ item.title }}</view>
<view
class="attach-title"
@tap="handlePreview(item)"
:style="{
backgroundColor: item.isSign ? '#07c160' : '#ed7b2f',
}"
>
{{ item.title }}
</view>
<up-button
text="预览"
type="primary"
:text="item.isSign ? '已签订' : '未签订'"
:type="item.isSign ? 'primary' : 'warning'"
size="small"
plain
:customStyle="{ width: '160rpx' }"
@tap="handlePreview(item)"
/>
@ -308,19 +368,25 @@
>
<view class="signature-modal">
<Signature
:height="380"
ref="signatureRef"
:height="300"
@save="handleSignatureSave"
@clear="handleSignatureClear"
/>
<view class="signature-modal-footer">
<up-button
text="关闭"
type="default"
type="primary"
plain
size="small"
:customStyle="{ marginTop: '24rpx' }"
@tap="handleCloseSignatureModal"
/>
<up-button
text="确认"
type="primary"
size="small"
@tap="handleConfirmSignature"
/>
</view>
</view>
</up-popup>
@ -345,16 +411,36 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getContentStyle } from '@/utils/safeArea'
import { realNameHttp } from '@/utils/realNameHttp'
import {
getWorkerInfoByIdNumberAPI,
getFileAddressAPI,
previewContractAPI,
} from '@/services/realName/contract'
import dayjs from 'dayjs'
import NavBarModal from '@/components/NavBarModal/index.vue'
import Signature from '@/components/Signature/index.vue'
import CommonPicker from '@/components/CommonPicker/index.vue'
import DatePicker from '@/components/DatePicker/index.vue'
import { getContentStyle } from '@/utils/safeArea'
import { getWorkerInfoByIdNumberAPI } from '@/services/realName/contract'
const iptIdNumber = ref('')
const videoInfo = ref(null)
const getFileAddressParams = ref({
perviewState: 0,
partA: '',
partB: '',
sex: '',
workAdress: '',
post: '',
partBIdCard: '',
partBPhone: '',
effectDate: '',
})
const contentStyle = computed(() => {
return getContentStyle({
includeNavBar: true,
@ -365,6 +451,7 @@ const contentStyle = computed(() => {
//
const form = ref({
proId: '',
partyA: '',
partAAdress: '',
legalPerson: '',
@ -373,12 +460,13 @@ const form = ref({
partBIdCard: '',
partBPhone: '',
partBAdress: '',
effectDate: '',
effectDate: dayjs().format('YYYY-MM-DD'),
workTask: '',
workAdress: '',
verMethod: '',
isXbg: '',
verMethod: '每天',
verStand: '',
achievementsVerification: '',
achievementsVerification: '每天',
bonusA: '',
bonusB: '',
otherSupply: '',
@ -389,8 +477,9 @@ const form = ref({
})
//
const startDateValue = ref('')
const startDateValue = ref(Date.now())
const handleStartDateChange = (val) => {
console.log(val)
startDateValue.value = val
const d = new Date(val)
const y = d.getFullYear()
@ -403,6 +492,19 @@ const handleStartDateChange = (val) => {
const payTypeOptions = ['每天']
const bonusTypeOptions = ['每天']
const xbgOptions = [
{
value: '0',
label: '否',
},
{
value: '1',
label: '是',
},
]
const handleSelectXbg = (val) => {
form.value.isXbg = val.value
}
const handleSelectPayType = (val) => {
form.value.verMethod = val.value
}
@ -430,24 +532,36 @@ const handleFaceDelete = () => {
// pdf
const attachments = ref([
{
title: '施工人员健康承诺书',
url: 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf',
isSign: false,
fileType: '1',
title: '施工人员健康承诺书',
},
{
url: 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf',
isSign: false,
fileType: '2',
title: '安全协议书',
url: 'https://www.africau.edu/images/default/sample.pdf',
},
{
title: '施工人员安全告知书',
url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
fileType: '3',
isSign: false,
// url: 'http://192.168.0.14:1917/hnAma/gzRealName/contract/pdf/.pdf',
// url: 'http://192.168.0.14:1917/hnAma/gzRealName/contract/pdf/.pdf',
url: 'http://192.168.0.38:18080/bnscloud/realnameapp/gzRealName/contract/pdf/施工人员安全告知书.pdf',
},
{
title: '签订用工协议承诺书',
url: 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf',
isSign: false,
fileType: '5',
title: '签订用工协议承诺书',
},
{
url: 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf',
isSign: false,
fileType: '4',
title: '安全承诺书',
url: 'https://www.africau.edu/images/default/sample.pdf',
},
])
@ -470,57 +584,198 @@ const handleBack = () => {
uni.navigateBack()
}
//
const captchaCountdown = ref(0)
let captchaTimer = null
//
const handleGetCaptcha = () => {
uni.$u.toast('验证码已发送(示例)')
if (captchaCountdown.value > 0) {
return
}
const captcha = generateCaptcha()
form.value.captcha = captcha
uni.$u.toast('验证码已生成,请查看输入框')
startCaptchaCountdown()
}
const startCaptchaCountdown = () => {
captchaCountdown.value = 60
captchaTimer && clearInterval(captchaTimer)
captchaTimer = setInterval(() => {
if (captchaCountdown.value <= 1) {
clearInterval(captchaTimer)
captchaTimer = null
captchaCountdown.value = 0
} else {
captchaCountdown.value -= 1
}
}, 1000)
}
// 40使
const generateCaptcha = () => {
// 使 crypto.getRandomValues 退 Math.random
const getSecureRandom = () => {
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
const buffer = new Uint32Array(1)
crypto.getRandomValues(buffer)
return buffer[0] % 10000
}
return Math.floor(Math.random() * 10000)
}
return getSecureRandom().toString().padStart(4, '0')
}
//
const handlePreview = (item) => {
if (!item.url) {
uni.$u.toast('附件地址缺失')
uni.$u.toast('附件地址缺失,请输入身份证号码,或者通过人脸识别获取附件内容')
return
}
const url = encodeURIComponent(item.url)
uni.navigateTo({
url: `/pages/work/contract/contractPreview/index?fileUrl=${url}&fileType=pdf&title=${encodeURIComponent(
item.title,
)}`,
url: `/pages/work/contract/contractPreview/index?fileUrl=${url}&fileType=pdf&title=${encodeURIComponent(item.title)}&attachmentType=${item.fileType}&isSign=${item.isSign}&personInfo=${encodeURIComponent(JSON.stringify(getFileAddressParams.value))}`,
events: {
signed: ({ fileType }) => {
console.log('收到签订事件:', { fileType })
const target = attachments.value.find((attach) => attach.fileType === fileType)
if (target) {
target.isSign = true
console.log('附件签订状态已更新:', target.title, target.isSign)
} else {
console.warn('未找到对应的附件:', fileType)
}
},
},
})
}
// H5 eventChannel 使
const handleSignedFallback = ({ fileType }) => {
const target = attachments.value.find((attach) => attach.fileType === fileType)
if (target) {
target.isSign = true
console.log('兜底事件更新附件签订状态:', target.title, target.isSign)
}
}
onMounted(() => {
uni.$on('contract-signed', handleSignedFallback)
})
onUnmounted(() => {
uni.$off('contract-signed', handleSignedFallback)
if (captchaTimer) {
clearInterval(captchaTimer)
captchaTimer = null
}
})
//
const handlePreviewContract = () => {
const handlePreviewContract = async () => {
//
const requiredFields = [
{ key: 'partyA', label: '甲方(公司名称)' },
{ key: 'partAAdress', label: '地址' },
{ key: 'legalPerson', label: '法定代表人' },
{ key: 'partAPhone', label: '联系电话' },
// { key: 'partAPhone', label: '' },
{ key: 'partyB', label: '乙方(劳动者姓名)' },
{ key: 'partBIdCard', label: '身份证号' },
{ key: 'partBPhone', label: '联系电话' },
{ key: 'startDate', label: '合同生效日期' },
{ key: 'effectDate', label: '合同生效日期' },
{ key: 'verStand', label: '工资核定标准' },
{ key: 'achievementsVerification', label: '绩效核定方式' },
{ key: 'bonusA', label: '绩效奖金区间(元) 起' },
{ key: 'bonusB', label: '绩效奖金区间(元) 止' },
]
for (const item of requiredFields) {
if (!form.value[item.key]) {
uni.$u.toast(`${item.label}不能为空`)
uni.$u.toast(
`${item.label}不能为空,请通过身份证号码查询或者人脸识别获取相关信息并完善表单`,
)
return
}
}
//
const bonusStart = Number(form.value.bonusA)
const bonusEnd = Number(form.value.bonusB)
if (Number.isNaN(bonusStart) || Number.isNaN(bonusEnd)) {
uni.$u.toast('绩效奖金区间需填写数字')
return
}
if (bonusStart > bonusEnd) {
uni.$u.toast('绩效奖金起始值不能大于结束值')
return
}
//
if (!form.value.signaturePath) {
uni.$u.toast('请先完成签名')
return
}
if (attachments.value.length === 0) {
uni.$u.toast('暂无合同附件')
//
const allSigned = attachments.value.every((item) => item.isSign)
if (!allSigned) {
uni.$u.toast('请先完成所有附件的签订')
return
}
handlePreview(attachments.value[0])
//
// if (!form.value.captcha) {
// uni.$u.toast('')
// return
// }
//
const message = form.value.captcha
const shortMessage = `【博诺思】您正在进行合同签订,验证码为:${message}有效期为5分钟。`
const nowDate = dayjs().format('YYYY-MM-DD')
const nowDateTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
const params = {
videoUrl: videoInfo.value?.videoUrl || '',
partA: form.value.partyA,
legalPerson: form.value.legalPerson,
partAPhone: form.value.partAPhone,
partAIdCard: null,
partAAdress: form.value.partAAdress,
partB: form.value.partyB,
partBPhone: form.value.partBPhone,
partBIdCard: form.value.partBIdCard,
partBAdress: form.value.partBAdress,
workTask: form.value.workTask,
workAdress: form.value.workAdress,
verMethod: form.value.verMethod,
verStand: form.value.verStand,
achievementsVerification: form.value.achievementsVerification,
bonusA: form.value.bonusA,
bonusB: form.value.bonusB,
effectDate: form.value.effectDate,
otherSupply: form.value.otherSupply,
otherMatters: form.value.otherMatters,
partBSign: form.value.signaturePath,
message,
signingDate: nowDate,
faceUrl: videoInfo.value?.faceUrl || '',
messageTime: nowDateTime,
shortMessage,
proId: videoInfo.value?.proId,
contractTemplateType: 1, //
agreedMethod: videoInfo.value?.agreedMethod || '',
secondContent: videoInfo.value?.secondContent || '',
isXbg: form.value.isXbg,
}
console.log('已组装的参数', params)
// const result = await previewContractAPI(params)
// handlePreview(attachments.value[0])
}
//
@ -547,11 +802,114 @@ const handleFaceRecognize = () => {
//
const handleGetWorkerInfo = async () => {
const { data: res } = await getWorkerInfoByIdNumberAPI({
const { obj: res } = await getWorkerInfoByIdNumberAPI({
idNumber: iptIdNumber.value,
})
console.log(res, '用户信息')
if (res !== 'is null') {
const {
name,
address,
subPhone,
phone,
idNumber,
postId,
postName,
represent,
subAddress,
subId,
sex,
subName,
isXbg,
proName,
} = res
form.value.partyA = subName
form.value.partAAdress = subAddress
form.value.partAPhone = subPhone
form.value.legalPerson = represent
form.value.partBPhone = phone
form.value.partyB = name
form.value.partBIdCard = idNumber
form.value.partBAdress = address
form.value.isXbg = isXbg
form.value.workAdress = proName
form.value.workTask = postName
getFileAddressParams.value.partA = subName
getFileAddressParams.value.partB = name
getFileAddressParams.value.sex = sex
getFileAddressParams.value.partBAdress = address
getFileAddressParams.value.workAdress = address
getFileAddressParams.value.post = postName
getFileAddressParams.value.partBIdCard = idNumber
getFileAddressParams.value.partBPhone = phone
getFileAddressParams.value.effectDate = form.value.effectDate
// const file1 = await getFileAddressFun('1') //
// const file2 = await getFileAddressFun('2') //
// const file4 = await getFileAddressFun('5') //
// const file5 = await getFileAddressFun('4') //
// const fileList = [file1, file2, file4, file5]
// attachments.value.forEach((e) => {
// const file = fileList.find((f) => f.type === e.fileType)
// if (file) {
// e.url = file.url
// }
// })
}
}
//
const getFileAddressFun = async (type) => {
const { resMsg: res } = await getFileAddressAPI({ ...getFileAddressParams.value, type })
return {
url: res,
type,
}
}
const handleConfirmSignature = async () => {
try {
if (signatureRef.value?.exportSignature) {
const tempPath = await signatureRef.value.exportSignature()
try {
const uploadRes = await realNameHttp.uploadFile({
url: '/user/uploadFile',
filePath: tempPath,
name: 'file',
formData: {
photoType: 1,
},
})
if (uploadRes.res === 1 && uploadRes.obj) {
form.value.signaturePath = tempPath
showSignatureModal.value = false
} else {
uni.$u.toast(uploadRes.resMsg || '上传失败')
}
} catch (err) {}
}
} catch (error) {
//
return
}
}
/**
* 页面加载
*/
onLoad((options) => {
if (options.params) {
try {
videoInfo.value = JSON.parse(decodeURIComponent(options.params))
} catch {}
}
})
</script>
<style lang="scss" scoped>
@ -632,12 +990,13 @@ const handleGetWorkerInfo = async () => {
.sign-preview {
flex: 1;
min-height: 200rpx;
border: 1rpx solid #e5e5e5;
// min-height: 200rpx;
}
.sign-empty-box {
width: 100%;
height: 200rpx;
height: 260rpx;
background: #f5f5f5;
border-radius: 8rpx;
border: 2rpx solid #e5e5e5;
@ -645,7 +1004,7 @@ const handleGetWorkerInfo = async () => {
.sign-image {
width: 100%;
height: 200rpx;
height: 260rpx;
}
.sign-buttons {
@ -662,13 +1021,19 @@ const handleGetWorkerInfo = async () => {
}
.signature-modal-footer {
margin-top: 24rpx;
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.captcha-row {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.attach-row {
@ -680,9 +1045,13 @@ const handleGetWorkerInfo = async () => {
.attach-title {
font-size: 28rpx;
color: #333;
flex: 1;
padding: 12rpx 0;
margin-right: 16rpx;
flex: 1;
background-color: #07c160;
border-radius: 10rpx;
color: #fff;
text-align: center;
}
.button-section {

View File

@ -203,14 +203,9 @@ const handleDocumentError = (data) => {
* 填写合同
*/
const handleFillContract = () => {
// TODO:
// uni.showToast({
// title: '',
// icon: 'none',
// })
const params = encodeURIComponent(JSON.stringify(contractData.value))
uni.navigateTo({
url: '/pages/work/contract/contractDetails/index',
url: `/pages/work/contract/contractDetails/index?params=${params}`,
})
}

View File

@ -9,6 +9,7 @@
</NavBarModal>
<view class="content-wrapper" :style="contentStyle">
<view class="file-title">{{ title }}</view>
<DocumentPreview
:file-url="fileUrl"
file-type="pdf"
@ -17,17 +18,92 @@
:show-retry="true"
:show-download="true"
:container-style="{ width: '100%', height: '100%' }"
@load="handleDocumentLoad"
@error="handleDocumentError"
/>
</view>
<view class="bottom-bar">
<up-button
v-if="isSafeNotice"
:text="readButtonText"
type="primary"
:disabled="readCountdown > 0 || !!previewError"
:customStyle="primaryButtonStyle"
@tap="handleReadConfirm"
/>
<up-button
v-else
text="去签订"
type="primary"
:customStyle="primaryButtonStyle"
@tap="handleOpenSignatureModal"
/>
</view>
<up-popup
:show="showSignatureModal"
mode="center"
round="20"
:closeable="false"
@close="showSignatureModal = false"
customStyle="width: 90%; max-width: 620rpx;"
>
<view class="signature-modal">
<view v-if="isSafetyProtocol" class="protocol-section">
<view class="protocol-title">协议时间</view>
<view class="protocol-row">
<view class="protocol-label">开始时间</view>
<view class="protocol-value">{{ protocolStartText }}</view>
</view>
<view class="protocol-row">
<view class="protocol-label">结束时间</view>
<view class="protocol-picker">
<DatePicker
v-model="protocolEndDate"
format="YYYY-MM-DD"
placeholder="请选择结束时间"
:minDate="todayTimestamp"
:showArrow="true"
@change="handleEndDateChange"
/>
</view>
</view>
</view>
<Signature
:height="380"
ref="signatureRef"
@save="handleSignatureSave"
@clear="handleSignatureClear"
/>
<view class="signature-modal-footer">
<up-button
text="确认签订"
type="primary"
size="small"
:customStyle="{ flex: 1 }"
@tap="handleConfirmSignature"
/>
</view>
</view>
</up-popup>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, onUnmounted, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getContentStyle } from '@/utils/safeArea'
import { realNameHttp } from '@/utils/realNameHttp'
import { signProtocolAPI } from '@/services/realName/contract'
import dayjs from 'dayjs'
import NavBarModal from '@/components/NavBarModal/index.vue'
import DocumentPreview from '@/components/DocumentPreview/index.vue'
import { getContentStyle } from '@/utils/safeArea'
import Signature from '@/components/Signature/index.vue'
import DatePicker from '@/components/DatePicker/index.vue'
const contentStyle = computed(() => {
return getContentStyle({
@ -38,6 +114,36 @@ const contentStyle = computed(() => {
})
const fileUrl = ref('')
const signatureRef = ref(null)
const attachmentType = ref('')
const title = ref('')
const isSign = ref(false)
const personInfo = ref({})
const type = ref('')
const eventChannel = ref(null)
const previewError = ref('')
const showSignatureModal = ref(false)
const signaturePath = ref('')
const readCountdown = ref(10)
const countdownTimer = ref(null)
const protocolEndDate = ref(dayjs().startOf('day').valueOf())
const todayTimestamp = dayjs().startOf('day').valueOf()
const isSafeNotice = computed(() => attachmentType.value === '3')
const isSafetyProtocol = computed(() => attachmentType.value === '2')
const protocolStartText = computed(() => dayjs(todayTimestamp).format('YYYY-MM-DD'))
const readButtonText = computed(() =>
readCountdown.value > 0 ? `我已阅读完毕(${readCountdown.value}s)` : '我已阅读完毕',
)
const primaryButtonStyle = computed(() => ({
flex: 1,
backgroundColor: '#07c160',
borderColor: '#07c160',
height: '88rpx',
fontSize: '32rpx',
}))
const handleBack = () => {
uni.navigateBack()
@ -46,6 +152,14 @@ const handleBack = () => {
onLoad((options) => {
if (options.fileUrl) {
fileUrl.value = decodeURIComponent(options.fileUrl)
attachmentType.value = options.attachmentType || ''
title.value = options.title ? decodeURIComponent(options.title) : ''
isSign.value = options.isSign === 'true'
personInfo.value = options.personInfo
? JSON.parse(decodeURIComponent(options.personInfo))
: {}
eventChannel.value =
typeof uni.getOpenerEventChannel === 'function' ? uni.getOpenerEventChannel() : null
} else {
uni.$u.toast('缺少文件地址')
setTimeout(() => {
@ -53,6 +167,157 @@ onLoad((options) => {
}, 800)
}
})
const handleDocumentLoad = () => {
previewError.value = ''
if (isSafeNotice.value) {
startCountdown()
}
}
const handleDocumentError = (err) => {
previewError.value = err?.error || '文档加载失败'
stopCountdown()
}
const startCountdown = () => {
stopCountdown()
readCountdown.value = 10
countdownTimer.value = setInterval(() => {
if (readCountdown.value > 0) {
readCountdown.value -= 1
} else {
stopCountdown()
}
}, 1000)
}
const stopCountdown = () => {
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
}
const emitSigned = () => {
const payload = {
fileType: attachmentType.value,
title: title.value,
}
if (eventChannel.value?.emit) {
eventChannel.value.emit('signed', payload)
console.log('已发送签订事件(eventChannel):', payload)
} else {
console.warn('eventChannel 不可用,使用全局事件兜底通知父页面')
uni.$emit('contract-signed', payload)
}
}
const handleReadConfirm = () => {
if (readCountdown.value > 0) return
isSign.value = true
emitSigned()
uni.$u.toast('已阅读并确认')
uni.navigateBack()
}
const handleOpenSignatureModal = () => {
if (previewError.value) {
uni.$u.toast('附件加载失败,暂无法签订')
return
}
showSignatureModal.value = true
}
const handleCloseSignatureModal = () => {
showSignatureModal.value = false
}
const handleSignatureSave = (payload) => {
signaturePath.value = payload?.tempFilePath || ''
}
const handleSignatureClear = () => {
signaturePath.value = ''
}
const handleEndDateChange = (val) => {
if (val < todayTimestamp) {
protocolEndDate.value = todayTimestamp
uni.$u.toast('结束时间不能早于今天')
}
}
const handleConfirmSignature = async () => {
try {
if (signatureRef.value?.exportSignature) {
const tempPath = await signatureRef.value.exportSignature()
signaturePath.value = tempPath || ''
console.log('signaturePath', signaturePath.value)
try {
const uploadRes = await realNameHttp.uploadFile({
url: '/user/uploadFile',
filePath: tempPath,
name: 'file',
formData: {
photoType: 1,
},
})
if (uploadRes.res === 1 && uploadRes.obj) {
handleSignProtocol(uploadRes.obj, attachmentType.value)
} else {
uni.$u.toast(uploadRes.resMsg || '上传失败')
}
} catch (err) {}
}
} catch (error) {
//
return
}
}
//
const handleSignProtocol = async (signaturePath, type) => {
try {
//
const params = {
partBSign: signaturePath,
type,
perviewState: 1,
partA: personInfo.value.partA,
partB: personInfo.value.partB,
sex: personInfo.value.sex,
workAdress: personInfo.value.workAdress,
partBIdCard: personInfo.value.partBIdCard,
partBPhone: personInfo.value.partBPhone,
effectDate: personInfo.value.effectDate,
startDate: type == 2 ? protocolStartText.value : '',
endDate: type == 2 ? dayjs(protocolEndDate.value).format('YYYY-MM-DD') : '',
post: personInfo.value.post,
}
const result = await signProtocolAPI(params)
if (result.res === 1) {
//
emitSigned()
uni.$u.toast('签订协议成功')
//
setTimeout(() => {
uni.navigateBack()
}, 300)
} else {
uni.$u.toast(res.resMsg || '签订协议失败')
}
} catch (error) {
console.error('签订协议失败:', error)
uni.$u.toast('签订协议失败,请重试')
}
}
onUnmounted(() => {
stopCountdown()
})
</script>
<style lang="scss" scoped>
@ -64,10 +329,78 @@ onLoad((options) => {
}
.content-wrapper {
flex: 1;
// flex: 1;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
// overflow: hidden;
}
.file-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
padding: 12rpx 0;
text-align: center;
}
.bottom-bar {
padding: 24rpx 32rpx;
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
padding-bottom: calc(env(safe-area-inset-bottom, 16px) + 24rpx);
}
.signature-modal {
padding: 32rpx;
background: #fff;
border-radius: 16rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.signature-modal-footer {
display: flex;
align-items: center;
}
.protocol-section {
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 12rpx 0;
}
.protocol-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.protocol-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.protocol-label {
width: 160rpx;
color: #666;
font-size: 26rpx;
}
.protocol-value {
flex: 1;
padding: 16rpx 20rpx;
background: #f5f5f5;
border-radius: 8rpx;
color: #333;
font-size: 28rpx;
}
.protocol-picker {
flex: 1;
}
.back-btn {
@ -86,4 +419,3 @@ onLoad((options) => {
transform: scale(0.95);
}
</style>

View File

@ -139,13 +139,13 @@ const checkVideoSize = (size) => {
*/
const checkVideoDuration = (duration) => {
// 60
const maxDuration = 60
const maxDuration = 20
if (duration > maxDuration) {
uni.$u.toast(`视频时长过长,请选择不超过${maxDuration}秒的视频`)
return false
}
if (duration < 1) {
uni.$u.toast('视频时长过短,请选择至少1秒的视频')
if (duration < 2) {
uni.$u.toast('视频时长过短,请选择至少2秒的视频')
return false
}
return true

View File

@ -72,8 +72,9 @@
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import NavBarModal from '@/components/NavBarModal/index.vue'
import { getContentStyle } from '@/utils/safeArea'
import { realNameHttp } from '@/utils/realNameHttp'
import NavBarModal from '@/components/NavBarModal/index.vue'
const contentStyle = computed(() => {
return getContentStyle({
@ -190,26 +191,43 @@ const handleReRecord = () => {
/**
* 下一步 - 跳转到合同信息页面
*/
const handleNext = () => {
const handleNext = async () => {
if (!selectedTemplate.value) {
uni.$u.toast('请选择合同模板')
return
}
//
const contractData = encodeURIComponent(
JSON.stringify({
template: selectedTemplate.value,
templateValue: templateOptions.value.find(
(item) => item.text === selectedTemplate.value,
)?.value,
videoInfo: videoInfo.value,
}),
)
//
try {
const uploadRes = await realNameHttp.uploadFile({
url: '/user/uploadFile',
filePath: videoInfo.value.videoUrl,
name: 'file',
formData: {
photoType: 1,
},
})
uni.navigateTo({
url: `/pages/work/contract/contractInfo/index?contractData=${contractData}`,
})
if (uploadRes.res === 1 && uploadRes.obj) {
//
const contractData = encodeURIComponent(
JSON.stringify({
template: selectedTemplate.value,
templateValue: templateOptions.value.find(
(item) => item.text === selectedTemplate.value,
)?.value,
videoInfo: videoInfo.value,
videoPath: uploadRes.obj,
}),
)
uni.navigateTo({
url: `/pages/work/contract/contractInfo/index?contractData=${contractData}`,
})
} else {
uni.$u.toast(uploadRes.resMsg || '上传失败')
}
} catch (err) {}
}
/**

View File

@ -17,3 +17,19 @@ export const getFileAddressAPI = (data) => {
method: 'POST',
})
}
// 签订协议
export const signProtocolAPI = (data) => {
return realNameHttp({
url: `/workPerson/contractCnsPdf?${initParams(data)}`,
method: 'POST',
})
}
// 预览合同
export const previewContractAPI = (data) => {
return realNameHttp({
url: `/workPerson/contractPdf?${initParams(data)}`,
method: 'POST',
})
}

View File

@ -32,6 +32,7 @@ export const createHttpClient = ({ baseURL, clientTag, tokenSelector }) => {
}
options.timeout = 60000
options[contentType] = ''
options.header = {
...options.header,
}
@ -219,7 +220,10 @@ export const createHttpClient = ({ baseURL, clientTag, tokenSelector }) => {
url: '/pages/login/index',
})
reject(res)
} else if (responseData.code === 500 || responseData.status === 'error') {
} else if (
responseData.code === 500 ||
responseData.status === 'error'
) {
uni.showToast({
icon: 'none',
title: responseData?.msg || '上传失败',

View File

@ -24,7 +24,8 @@ export default defineConfig({
// 实名制系统代理规则
'/bmw': {
// target: 'http://192.168.0.234:1917/hnAma/',
target: 'http://192.168.0.14:1917/hnAma/',
// target: 'http://192.168.0.14:1917/hnAma/',
target: 'http://192.168.0.38:18080/bnscloud/realnameapp/',
changeOrigin: true,
rewrite: (path) => {
return path.replace(/\/bmw/, '')