feat:合同列表
This commit is contained in:
36
src/constants/contract-sign.ts
Normal file
36
src/constants/contract-sign.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export const CONTRACT_SIGN_STEPS = [
|
||||||
|
{ key: 'party-a', label: '甲方已签', status: 'done' as const },
|
||||||
|
{ key: 'party-b', label: '待乙方签署', status: 'active' as const },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const CONTRACT_SIGN_BODY = `软件定制开发合同
|
||||||
|
|
||||||
|
合同编号:【】
|
||||||
|
签订地点:【】
|
||||||
|
签订日期:【】年【】月【】日
|
||||||
|
|
||||||
|
甲方(委托方):
|
||||||
|
名称:【甲方公司全称】
|
||||||
|
统一社会信用代码:【】
|
||||||
|
地址:【】
|
||||||
|
联系人:【】
|
||||||
|
电话:【】
|
||||||
|
电子邮箱:【】
|
||||||
|
|
||||||
|
乙方(开发方):
|
||||||
|
名称:【乙方公司全称】
|
||||||
|
统一社会信用代码:【】
|
||||||
|
地址:【】
|
||||||
|
联系人:【】
|
||||||
|
电话:【】
|
||||||
|
电子邮箱:【】
|
||||||
|
|
||||||
|
鉴于甲方拟委托乙方进行软件定制开发服务,双方依据《中华人民共和国民法典》及相关法律法规,在平等、自愿、公平和诚实信用的基础上,经友好协商,达成如下协议……`
|
||||||
|
|
||||||
|
export const CONTRACT_SIGN_SIGNATURE_STORAGE_KEY = 'contract-sign-signature-preview'
|
||||||
|
|
||||||
|
export const CONTRACT_SIGN_PAYMENT_INFO = {
|
||||||
|
method: '微信支付',
|
||||||
|
paidAt: '2026-01-21 15:00:00',
|
||||||
|
transactionNo: 'T20260819153035678K2M001',
|
||||||
|
}
|
||||||
@@ -6,10 +6,23 @@ export const CONTRACT_TABS: Array<{ value: ContractTab, label: string }> = [
|
|||||||
{ value: 'signed', label: '已签署' },
|
{ value: 'signed', label: '已签署' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const CONTRACT_DETAIL_BASE = {
|
||||||
|
contractName: '社区康养智能服务系统开发合同',
|
||||||
|
contractType: '互联网软件开发',
|
||||||
|
contractNo: 'C8K2M202408001',
|
||||||
|
signDate: '2024-10-15',
|
||||||
|
effectiveDate: '2024-10-15',
|
||||||
|
expiryDate: '2024-10-15',
|
||||||
|
relatedProject: '社区康养智能服务系统',
|
||||||
|
contractStatus: '履行中',
|
||||||
|
projectId: '1',
|
||||||
|
fileName: '需求文档.pdf',
|
||||||
|
}
|
||||||
|
|
||||||
export const CONTRACT_LIST: ContractItem[] = [
|
export const CONTRACT_LIST: ContractItem[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
contractNo: 'C8K2M202408001',
|
contractNo: CONTRACT_DETAIL_BASE.contractNo,
|
||||||
title: '社区康养智慧系统',
|
title: '社区康养智慧系统',
|
||||||
type: '软件开发',
|
type: '软件开发',
|
||||||
relatedLabel: '关联合同',
|
relatedLabel: '关联合同',
|
||||||
@@ -21,10 +34,11 @@ export const CONTRACT_LIST: ContractItem[] = [
|
|||||||
],
|
],
|
||||||
showDownload: true,
|
showDownload: true,
|
||||||
showSign: false,
|
showSign: false,
|
||||||
|
...CONTRACT_DETAIL_BASE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
contractNo: 'C8K2M202408001',
|
contractNo: CONTRACT_DETAIL_BASE.contractNo,
|
||||||
title: '社区康养智慧系统',
|
title: '社区康养智慧系统',
|
||||||
type: '软件开发',
|
type: '软件开发',
|
||||||
relatedLabel: '关联项目',
|
relatedLabel: '关联项目',
|
||||||
@@ -35,10 +49,12 @@ export const CONTRACT_LIST: ContractItem[] = [
|
|||||||
],
|
],
|
||||||
showDownload: false,
|
showDownload: false,
|
||||||
showSign: true,
|
showSign: true,
|
||||||
|
...CONTRACT_DETAIL_BASE,
|
||||||
|
contractStatus: '待签署',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
contractNo: 'C8K2M202408001',
|
contractNo: CONTRACT_DETAIL_BASE.contractNo,
|
||||||
title: '社区康养智慧系统',
|
title: '社区康养智慧系统',
|
||||||
type: '软件开发',
|
type: '软件开发',
|
||||||
relatedLabel: '关联项目',
|
relatedLabel: '关联项目',
|
||||||
@@ -49,5 +65,10 @@ export const CONTRACT_LIST: ContractItem[] = [
|
|||||||
],
|
],
|
||||||
showDownload: false,
|
showDownload: false,
|
||||||
showSign: false,
|
showSign: false,
|
||||||
|
...CONTRACT_DETAIL_BASE,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export function getContract(id: string): ContractItem | null {
|
||||||
|
return CONTRACT_LIST.find(item => item.id === id) || null
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import iconInvoice from '@/static/icon/invoice.png'
|
|||||||
import iconMessagePush from '@/static/icon/message-push.png'
|
import iconMessagePush from '@/static/icon/message-push.png'
|
||||||
import iconQualification from '@/static/icon/qualification.png'
|
import iconQualification from '@/static/icon/qualification.png'
|
||||||
import iconSecurity from '@/static/icon/security.png'
|
import iconSecurity from '@/static/icon/security.png'
|
||||||
import iconServiceHotline from '@/static/icon/service-hotline.svg'
|
import iconServiceHotline from '@/static/icon/service-hotline.png'
|
||||||
|
|
||||||
export const PERSONAL_CENTER_PROFILE: PersonalCenterProfile = {
|
export const PERSONAL_CENTER_PROFILE: PersonalCenterProfile = {
|
||||||
nickname: '这里是昵称',
|
nickname: '这里是昵称',
|
||||||
|
|||||||
@@ -223,6 +223,24 @@
|
|||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/contract-center/detail/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/contract-center/sign/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/contract-center/sign-success/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/personal-center/service-hotline/index",
|
"path": "pages/personal-center/service-hotline/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
132
src/pages/contract-center/detail/index.scss
Normal file
132
src/pages/contract-center/detail/index.scss
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
@import '@/styles/variables.scss';
|
||||||
|
|
||||||
|
.contract-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg-section;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__navbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 20rpx;
|
||||||
|
background: $color-bg-page;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__back {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__back-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__navbar-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__navbar-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__content {
|
||||||
|
padding: 20rpx 24rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__card {
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
background: $color-bg-page;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__section {
|
||||||
|
&--divider {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
padding-top: 32rpx;
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__section-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__info-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__info-value {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 24rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $color-text-primary;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__file {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 160rpx;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
background: $color-bg-section;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__file-icon {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__file-name {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $color-text-primary;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__link-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-detail__link {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
144
src/pages/contract-center/detail/index.vue
Normal file
144
src/pages/contract-center/detail/index.vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { getContract } from '@/constants/contract'
|
||||||
|
import type { ContractItem } from '@/types/contract'
|
||||||
|
import pdfIcon from '@/static/icons/PDF.png'
|
||||||
|
import { navigateBackWithFallback } from '@/utils/navigate-back'
|
||||||
|
|
||||||
|
const contract = ref<ContractItem | null>(null)
|
||||||
|
|
||||||
|
const infoRows = [
|
||||||
|
{ label: '合同名称', key: 'contractName' as const },
|
||||||
|
{ label: '合同类型', key: 'contractType' as const },
|
||||||
|
{ label: '合同编号', key: 'contractNo' as const },
|
||||||
|
{ label: '签订日期', key: 'signDate' as const },
|
||||||
|
{ label: '生效日期', key: 'effectiveDate' as const },
|
||||||
|
{ label: '到期日期', key: 'expiryDate' as const },
|
||||||
|
{ label: '关联项目', key: 'relatedProject' as const },
|
||||||
|
{ label: '合同状态', key: 'contractStatus' as const },
|
||||||
|
]
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
const rawId = options?.id
|
||||||
|
const id = rawId ? decodeURIComponent(String(rawId)) : '1'
|
||||||
|
contract.value = getContract(id)
|
||||||
|
|
||||||
|
if (!contract.value) {
|
||||||
|
uni.showToast({ title: '合同不存在', icon: 'none' })
|
||||||
|
setTimeout(() => uni.navigateBack(), 1500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigateBackWithFallback('/pages/contract-center/index')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewProject() {
|
||||||
|
if (!contract.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/project-center/detail/index?id=${encodeURIComponent(contract.value.projectId)}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view
|
||||||
|
v-if="contract"
|
||||||
|
class="contract-detail"
|
||||||
|
>
|
||||||
|
<view class="contract-detail__navbar">
|
||||||
|
<view
|
||||||
|
class="contract-detail__back"
|
||||||
|
@tap="handleBack"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="contract-detail__back-icon"
|
||||||
|
src="/static/icons/icon-back.svg"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text class="contract-detail__navbar-title">
|
||||||
|
合同详情
|
||||||
|
</text>
|
||||||
|
<view class="contract-detail__navbar-placeholder" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view
|
||||||
|
class="contract-detail__scroll"
|
||||||
|
scroll-y
|
||||||
|
:show-scrollbar="false"
|
||||||
|
>
|
||||||
|
<view class="contract-detail__content">
|
||||||
|
<view class="contract-detail__card">
|
||||||
|
<view class="contract-detail__section">
|
||||||
|
<view
|
||||||
|
v-for="row in infoRows"
|
||||||
|
:key="row.key"
|
||||||
|
class="contract-detail__info-row"
|
||||||
|
>
|
||||||
|
<text class="contract-detail__info-label">
|
||||||
|
{{ row.label }}
|
||||||
|
</text>
|
||||||
|
<text class="contract-detail__info-value">
|
||||||
|
{{ contract[row.key] }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="contract-detail__section contract-detail__section--divider">
|
||||||
|
<text class="contract-detail__section-title">
|
||||||
|
合同文件
|
||||||
|
</text>
|
||||||
|
<view class="contract-detail__file">
|
||||||
|
<image
|
||||||
|
class="contract-detail__file-icon"
|
||||||
|
:src="pdfIcon"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<text class="contract-detail__file-name">
|
||||||
|
{{ contract.fileName }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="contract-detail__section contract-detail__section--divider">
|
||||||
|
<text class="contract-detail__section-title">
|
||||||
|
关联项目
|
||||||
|
</text>
|
||||||
|
<view class="contract-detail__info-row">
|
||||||
|
<text class="contract-detail__info-label">
|
||||||
|
关联项目
|
||||||
|
</text>
|
||||||
|
<text class="contract-detail__info-value">
|
||||||
|
{{ contract.relatedProject }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="contract-detail__info-row">
|
||||||
|
<text class="contract-detail__info-label">
|
||||||
|
合同状态
|
||||||
|
</text>
|
||||||
|
<text class="contract-detail__info-value">
|
||||||
|
{{ contract.contractStatus }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="contract-detail__link-row">
|
||||||
|
<text
|
||||||
|
class="contract-detail__link"
|
||||||
|
@tap="handleViewProject"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import './index.scss';
|
||||||
|
</style>
|
||||||
@@ -34,7 +34,9 @@ function badgeClass(type: ContractItem['badges'][number]['type']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleViewDetail(item: ContractItem) {
|
function handleViewDetail(item: ContractItem) {
|
||||||
uni.showToast({ title: `查看${item.title}详情开发中`, icon: 'none' })
|
uni.navigateTo({
|
||||||
|
url: `/pages/contract-center/detail/index?id=${encodeURIComponent(item.id)}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePreview(item: ContractItem) {
|
function handlePreview(item: ContractItem) {
|
||||||
@@ -46,7 +48,9 @@ function handleDownload(item: ContractItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSign(item: ContractItem) {
|
function handleSign(item: ContractItem) {
|
||||||
uni.showToast({ title: `签署${item.contractNo}开发中`, icon: 'none' })
|
uni.navigateTo({
|
||||||
|
url: `/pages/contract-center/sign/index?id=${encodeURIComponent(item.id)}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
171
src/pages/contract-center/sign-success/index.scss
Normal file
171
src/pages/contract-center/sign-success/index.scss
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
@import '@/styles/variables.scss';
|
||||||
|
|
||||||
|
.sign-success {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg-section;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__navbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 20rpx;
|
||||||
|
background: $color-bg-page;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__back {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__back-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__navbar-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__navbar-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__content {
|
||||||
|
padding: 20rpx 24rpx calc(140rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__hero {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40rpx 24rpx 36rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
background: $color-bg-page;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__hero-icon {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__hero-title {
|
||||||
|
margin-top: 28rpx;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__hero-subtitle {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__card {
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
background: $color-bg-page;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
|
||||||
|
&--link {
|
||||||
|
padding: 32rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__card-link-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__card-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__info-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__info-value {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 24rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $color-text-primary;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__signature-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 200rpx;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1rpx solid #f0f0f0;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__signature-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 200rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||||
|
background: $color-bg-page;
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-success__footer-btn {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 88rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
background: $color-primary;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
184
src/pages/contract-center/sign-success/index.vue
Normal file
184
src/pages/contract-center/sign-success/index.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import {
|
||||||
|
CONTRACT_SIGN_PAYMENT_INFO,
|
||||||
|
CONTRACT_SIGN_SIGNATURE_STORAGE_KEY,
|
||||||
|
} from '@/constants/contract-sign'
|
||||||
|
import { getContract } from '@/constants/contract'
|
||||||
|
import type { ContractItem } from '@/types/contract'
|
||||||
|
import successIcon from '@/static/icons/icon-register-success.png'
|
||||||
|
import { navigateBackWithFallback } from '@/utils/navigate-back'
|
||||||
|
|
||||||
|
const contract = ref<ContractItem | null>(null)
|
||||||
|
const signaturePreview = ref('')
|
||||||
|
|
||||||
|
const paymentInfo = CONTRACT_SIGN_PAYMENT_INFO
|
||||||
|
|
||||||
|
const successSubtitle = computed(() => {
|
||||||
|
if (!contract.value)
|
||||||
|
return ''
|
||||||
|
return `您已成功签署《${contract.value.contractName}》`
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
const rawId = options?.id
|
||||||
|
const id = rawId ? decodeURIComponent(String(rawId)) : '2'
|
||||||
|
contract.value = getContract(id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = uni.getStorageSync(CONTRACT_SIGN_SIGNATURE_STORAGE_KEY)
|
||||||
|
if (typeof stored === 'string' && stored)
|
||||||
|
signaturePreview.value = stored
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignore storage read errors
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contract.value) {
|
||||||
|
uni.showToast({ title: '合同不存在', icon: 'none' })
|
||||||
|
setTimeout(() => uni.navigateBack(), 1500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigateBackWithFallback('/pages/contract-center/index')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewCertificate() {
|
||||||
|
uni.showToast({ title: '电子签署凭证开发中', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBackToList() {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/contract-center/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewDetail() {
|
||||||
|
if (!contract.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/contract-center/detail/index?id=${encodeURIComponent(contract.value.id)}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view
|
||||||
|
v-if="contract"
|
||||||
|
class="sign-success"
|
||||||
|
>
|
||||||
|
<view class="sign-success__navbar">
|
||||||
|
<view
|
||||||
|
class="sign-success__back"
|
||||||
|
@tap="handleBack"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="sign-success__back-icon"
|
||||||
|
src="/static/icons/icon-back.svg"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text class="sign-success__navbar-title">
|
||||||
|
签署验证
|
||||||
|
</text>
|
||||||
|
<view class="sign-success__navbar-placeholder" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view
|
||||||
|
class="sign-success__scroll"
|
||||||
|
scroll-y
|
||||||
|
:show-scrollbar="false"
|
||||||
|
>
|
||||||
|
<view class="sign-success__content">
|
||||||
|
<view class="sign-success__hero">
|
||||||
|
<image
|
||||||
|
class="sign-success__hero-icon"
|
||||||
|
:src="successIcon"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<text class="sign-success__hero-title">
|
||||||
|
签署成功
|
||||||
|
</text>
|
||||||
|
<text class="sign-success__hero-subtitle">
|
||||||
|
{{ successSubtitle }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
class="sign-success__card sign-success__card--link"
|
||||||
|
@tap="handleViewCertificate"
|
||||||
|
>
|
||||||
|
<text class="sign-success__card-link-text">
|
||||||
|
电子签署凭证
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="sign-success__card">
|
||||||
|
<text class="sign-success__card-title">
|
||||||
|
支付信息
|
||||||
|
</text>
|
||||||
|
<view class="sign-success__info-row">
|
||||||
|
<text class="sign-success__info-label">
|
||||||
|
支付方式
|
||||||
|
</text>
|
||||||
|
<text class="sign-success__info-value">
|
||||||
|
{{ paymentInfo.method }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="sign-success__info-row">
|
||||||
|
<text class="sign-success__info-label">
|
||||||
|
支付时间
|
||||||
|
</text>
|
||||||
|
<text class="sign-success__info-value">
|
||||||
|
{{ paymentInfo.paidAt }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="sign-success__info-row">
|
||||||
|
<text class="sign-success__info-label">
|
||||||
|
交易流水号
|
||||||
|
</text>
|
||||||
|
<text class="sign-success__info-value">
|
||||||
|
{{ paymentInfo.transactionNo }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="sign-success__card">
|
||||||
|
<text class="sign-success__card-title">
|
||||||
|
您的签名
|
||||||
|
</text>
|
||||||
|
<view class="sign-success__signature-preview">
|
||||||
|
<image
|
||||||
|
v-if="signaturePreview"
|
||||||
|
class="sign-success__signature-image"
|
||||||
|
:src="signaturePreview"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="sign-success__footer">
|
||||||
|
<view
|
||||||
|
class="sign-success__footer-btn"
|
||||||
|
@tap="handleBackToList"
|
||||||
|
>
|
||||||
|
返回合同列表
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="sign-success__footer-btn"
|
||||||
|
@tap="handleViewDetail"
|
||||||
|
>
|
||||||
|
查看合同详情
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import './index.scss';
|
||||||
|
</style>
|
||||||
88
src/pages/contract-center/sign/components/SignaturePad.scss
Normal file
88
src/pages/contract-center/sign/components/SignaturePad.scss
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
@import '@/styles/variables.scss';
|
||||||
|
|
||||||
|
.signature-pad {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 400;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-pad__body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48rpx 32rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-pad__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-pad__action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 72rpx;
|
||||||
|
padding: 32rpx 16rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
background: $color-primary;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-pad__canvas-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 520rpx;
|
||||||
|
height: 72vh;
|
||||||
|
background: $color-bg-page;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-pad__canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-pad__placeholder {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #e5e7eb;
|
||||||
|
transform: translate(-50%, -50%) rotate(90deg);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-pad__close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: flex-end;
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
margin: 0 32rpx 48rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-pad__close-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
line-height: 1;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
168
src/pages/contract-center/sign/components/SignaturePad.vue
Normal file
168
src/pages/contract-center/sign/components/SignaturePad.vue
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getCurrentInstance, nextTick, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
confirm: [imagePath: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
const hasStroke = ref(false)
|
||||||
|
const canvasWidth = ref(300)
|
||||||
|
const canvasHeight = ref(500)
|
||||||
|
|
||||||
|
let ctx: UniApp.CanvasContext | null = null
|
||||||
|
let drawing = false
|
||||||
|
let lastX = 0
|
||||||
|
let lastY = 0
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (visible) => {
|
||||||
|
if (!visible)
|
||||||
|
return
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
const query = uni.createSelectorQuery().in(instance)
|
||||||
|
query.select('#signature-canvas').boundingClientRect((rect) => {
|
||||||
|
if (rect && !Array.isArray(rect)) {
|
||||||
|
canvasWidth.value = rect.width
|
||||||
|
canvasHeight.value = rect.height
|
||||||
|
}
|
||||||
|
initCanvas()
|
||||||
|
}).exec()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
ctx = uni.createCanvasContext('signature-canvas', instance)
|
||||||
|
ctx.setStrokeStyle('#1a1a1a')
|
||||||
|
ctx.setLineWidth(3)
|
||||||
|
ctx.setLineCap('round')
|
||||||
|
ctx.setLineJoin('round')
|
||||||
|
clearCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCanvas() {
|
||||||
|
if (!ctx)
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
|
||||||
|
ctx.draw()
|
||||||
|
hasStroke.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTouchPoint(event: TouchEvent) {
|
||||||
|
const touch = event.touches[0] || event.changedTouches[0]
|
||||||
|
return { x: touch.x, y: touch.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchStart(event: TouchEvent) {
|
||||||
|
if (!ctx)
|
||||||
|
return
|
||||||
|
|
||||||
|
const point = getTouchPoint(event)
|
||||||
|
lastX = point.x
|
||||||
|
lastY = point.y
|
||||||
|
drawing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchMove(event: TouchEvent) {
|
||||||
|
if (!drawing || !ctx)
|
||||||
|
return
|
||||||
|
|
||||||
|
const point = getTouchPoint(event)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(lastX, lastY)
|
||||||
|
ctx.lineTo(point.x, point.y)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.draw(true)
|
||||||
|
lastX = point.x
|
||||||
|
lastY = point.y
|
||||||
|
hasStroke.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchEnd() {
|
||||||
|
drawing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
if (!hasStroke.value) {
|
||||||
|
uni.showToast({ title: '请先签名', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.canvasToTempFilePath({
|
||||||
|
canvasId: 'signature-canvas',
|
||||||
|
success: (res) => {
|
||||||
|
emit('confirm', res.tempFilePath)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
emit('confirm', '')
|
||||||
|
},
|
||||||
|
}, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view
|
||||||
|
v-if="visible"
|
||||||
|
class="signature-pad"
|
||||||
|
>
|
||||||
|
<view class="signature-pad__body">
|
||||||
|
<view class="signature-pad__actions">
|
||||||
|
<view
|
||||||
|
class="signature-pad__action-btn"
|
||||||
|
@tap="clearCanvas"
|
||||||
|
>
|
||||||
|
清除重写
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="signature-pad__action-btn"
|
||||||
|
@tap="handleConfirm"
|
||||||
|
>
|
||||||
|
确认签署
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="signature-pad__canvas-wrap">
|
||||||
|
<canvas
|
||||||
|
id="signature-canvas"
|
||||||
|
canvas-id="signature-canvas"
|
||||||
|
class="signature-pad__canvas"
|
||||||
|
disable-scroll
|
||||||
|
@touchstart.stop.prevent="handleTouchStart"
|
||||||
|
@touchmove.stop.prevent="handleTouchMove"
|
||||||
|
@touchend.stop.prevent="handleTouchEnd"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
v-if="!hasStroke"
|
||||||
|
class="signature-pad__placeholder"
|
||||||
|
>
|
||||||
|
我是签名
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
class="signature-pad__close"
|
||||||
|
@tap="handleClose"
|
||||||
|
>
|
||||||
|
<text class="signature-pad__close-icon">
|
||||||
|
×
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import './SignaturePad.scss';
|
||||||
|
</style>
|
||||||
198
src/pages/contract-center/sign/index.scss
Normal file
198
src/pages/contract-center/sign/index.scss
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
@import '@/styles/variables.scss';
|
||||||
|
|
||||||
|
.contract-sign {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg-section;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__navbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 20rpx;
|
||||||
|
background: $color-bg-page;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__back {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__back-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__navbar-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__navbar-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 0 24rpx 20rpx;
|
||||||
|
background: $color-bg-page;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__contract-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__steps {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 48rpx 28rpx;
|
||||||
|
background: $color-bg-page;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__step {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__step-track {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__step-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background: #e5e7eb;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&--done,
|
||||||
|
&--active {
|
||||||
|
background: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__step-check {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__step-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 4rpx;
|
||||||
|
margin: 0 8rpx;
|
||||||
|
background: #e5e7eb;
|
||||||
|
|
||||||
|
&--done {
|
||||||
|
background: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__step-label {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__content {
|
||||||
|
padding: 24rpx 24rpx calc(140rpx + env(safe-area-inset-bottom));
|
||||||
|
background: $color-bg-page;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__body {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: $color-text-primary;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__signature-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
margin-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__signature-col {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__signature-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__signature-box {
|
||||||
|
height: 160rpx;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
|
||||||
|
&--done {
|
||||||
|
background: #eef2ff;
|
||||||
|
border: 1rpx solid $color-primary-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||||
|
background: $color-bg-page;
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-sign__submit-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 88rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
background: $color-primary;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
182
src/pages/contract-center/sign/index.vue
Normal file
182
src/pages/contract-center/sign/index.vue
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import {
|
||||||
|
CONTRACT_SIGN_BODY,
|
||||||
|
CONTRACT_SIGN_SIGNATURE_STORAGE_KEY,
|
||||||
|
CONTRACT_SIGN_STEPS,
|
||||||
|
} from '@/constants/contract-sign'
|
||||||
|
import { getContract } from '@/constants/contract'
|
||||||
|
import type { ContractItem } from '@/types/contract'
|
||||||
|
import { navigateBackWithFallback } from '@/utils/navigate-back'
|
||||||
|
import SignaturePad from './components/SignaturePad.vue'
|
||||||
|
|
||||||
|
const contract = ref<ContractItem | null>(null)
|
||||||
|
const signSteps = CONTRACT_SIGN_STEPS
|
||||||
|
const contractBody = CONTRACT_SIGN_BODY
|
||||||
|
const signaturePadVisible = ref(false)
|
||||||
|
const partyBSignaturePreview = ref(false)
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
const rawId = options?.id
|
||||||
|
const id = rawId ? decodeURIComponent(String(rawId)) : '2'
|
||||||
|
contract.value = getContract(id)
|
||||||
|
|
||||||
|
if (!contract.value) {
|
||||||
|
uni.showToast({ title: '合同不存在', icon: 'none' })
|
||||||
|
setTimeout(() => uni.navigateBack(), 1500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigateBackWithFallback('/pages/contract-center/index')
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSignaturePad() {
|
||||||
|
signaturePadVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSignaturePad() {
|
||||||
|
signaturePadVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSignConfirm(imagePath: string) {
|
||||||
|
if (!contract.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
partyBSignaturePreview.value = true
|
||||||
|
signaturePadVisible.value = false
|
||||||
|
|
||||||
|
if (imagePath) {
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(CONTRACT_SIGN_SIGNATURE_STORAGE_KEY, imagePath)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignore storage write errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.redirectTo({
|
||||||
|
url: `/pages/contract-center/sign-success/index?id=${encodeURIComponent(contract.value.id)}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view
|
||||||
|
v-if="contract"
|
||||||
|
class="contract-sign"
|
||||||
|
>
|
||||||
|
<view class="contract-sign__navbar">
|
||||||
|
<view
|
||||||
|
class="contract-sign__back"
|
||||||
|
@tap="handleBack"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="contract-sign__back-icon"
|
||||||
|
src="/static/icons/icon-back.svg"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text class="contract-sign__navbar-title">
|
||||||
|
合同签署
|
||||||
|
</text>
|
||||||
|
<view class="contract-sign__navbar-placeholder" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="contract-sign__header">
|
||||||
|
<text class="contract-sign__contract-name">
|
||||||
|
{{ contract.contractName }}
|
||||||
|
</text>
|
||||||
|
<text class="contract-sign__status">
|
||||||
|
待签署
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="contract-sign__steps">
|
||||||
|
<view
|
||||||
|
v-for="(step, index) in signSteps"
|
||||||
|
:key="step.key"
|
||||||
|
class="contract-sign__step"
|
||||||
|
>
|
||||||
|
<view class="contract-sign__step-track">
|
||||||
|
<view
|
||||||
|
class="contract-sign__step-node"
|
||||||
|
:class="{
|
||||||
|
'contract-sign__step-node--done': step.status === 'done',
|
||||||
|
'contract-sign__step-node--active': step.status === 'active',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
v-if="step.status === 'done'"
|
||||||
|
class="contract-sign__step-check"
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="index < signSteps.length - 1"
|
||||||
|
class="contract-sign__step-line"
|
||||||
|
:class="{ 'contract-sign__step-line--done': step.status === 'done' }"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text
|
||||||
|
class="contract-sign__step-label"
|
||||||
|
:class="{ 'contract-sign__step-label--active': step.status === 'active' }"
|
||||||
|
>
|
||||||
|
{{ step.label }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view
|
||||||
|
class="contract-sign__scroll"
|
||||||
|
scroll-y
|
||||||
|
:show-scrollbar="false"
|
||||||
|
>
|
||||||
|
<view class="contract-sign__content">
|
||||||
|
<text class="contract-sign__body">
|
||||||
|
{{ contractBody }}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<view class="contract-sign__signature-row">
|
||||||
|
<view class="contract-sign__signature-col">
|
||||||
|
<text class="contract-sign__signature-label">
|
||||||
|
甲方(签名/盖章):
|
||||||
|
</text>
|
||||||
|
<view class="contract-sign__signature-box contract-sign__signature-box--done" />
|
||||||
|
</view>
|
||||||
|
<view class="contract-sign__signature-col">
|
||||||
|
<text class="contract-sign__signature-label">
|
||||||
|
乙方(签名/盖章):
|
||||||
|
</text>
|
||||||
|
<view
|
||||||
|
class="contract-sign__signature-box"
|
||||||
|
:class="{ 'contract-sign__signature-box--done': partyBSignaturePreview }"
|
||||||
|
@tap="openSignaturePad"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="contract-sign__footer">
|
||||||
|
<view
|
||||||
|
class="contract-sign__submit-btn"
|
||||||
|
@tap="openSignaturePad"
|
||||||
|
>
|
||||||
|
立即签署
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<SignaturePad
|
||||||
|
:visible="signaturePadVisible"
|
||||||
|
@close="closeSignaturePad"
|
||||||
|
@confirm="handleSignConfirm"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import './index.scss';
|
||||||
|
</style>
|
||||||
@@ -61,6 +61,10 @@ function handleQuickEntryTap(entry: QuickEntryItem) {
|
|||||||
navigateQuickEntry(entry.id, entry.label)
|
navigateQuickEntry(entry.id, entry.label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleViewAllProjects() {
|
||||||
|
navigateQuickEntry('project', '项目中心')
|
||||||
|
}
|
||||||
|
|
||||||
const projects: ProjectItem[] = [
|
const projects: ProjectItem[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -187,7 +191,10 @@ const projects: ProjectItem[] = [
|
|||||||
<text class="home__section-title">
|
<text class="home__section-title">
|
||||||
在办项目
|
在办项目
|
||||||
</text>
|
</text>
|
||||||
<text class="home__section-action">
|
<text
|
||||||
|
class="home__section-action"
|
||||||
|
@tap="handleViewAllProjects"
|
||||||
|
>
|
||||||
查看全部
|
查看全部
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -20,4 +20,13 @@ export interface ContractItem {
|
|||||||
badges: ContractBadge[]
|
badges: ContractBadge[]
|
||||||
showDownload: boolean
|
showDownload: boolean
|
||||||
showSign: boolean
|
showSign: boolean
|
||||||
|
contractName: string
|
||||||
|
contractType: string
|
||||||
|
signDate: string
|
||||||
|
effectiveDate: string
|
||||||
|
expiryDate: string
|
||||||
|
relatedProject: string
|
||||||
|
contractStatus: string
|
||||||
|
projectId: string
|
||||||
|
fileName: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user