feat:合同列表

This commit is contained in:
李伊帆
2026-09-17 15:48:51 +08:00
parent 8394da7ecb
commit 0e80de4548
15 changed files with 1369 additions and 7 deletions

View 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',
}

View File

@@ -6,10 +6,23 @@ export const CONTRACT_TABS: Array<{ value: ContractTab, label: string }> = [
{ 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[] = [
{
id: '1',
contractNo: 'C8K2M202408001',
contractNo: CONTRACT_DETAIL_BASE.contractNo,
title: '社区康养智慧系统',
type: '软件开发',
relatedLabel: '关联合同',
@@ -21,10 +34,11 @@ export const CONTRACT_LIST: ContractItem[] = [
],
showDownload: true,
showSign: false,
...CONTRACT_DETAIL_BASE,
},
{
id: '2',
contractNo: 'C8K2M202408001',
contractNo: CONTRACT_DETAIL_BASE.contractNo,
title: '社区康养智慧系统',
type: '软件开发',
relatedLabel: '关联项目',
@@ -35,10 +49,12 @@ export const CONTRACT_LIST: ContractItem[] = [
],
showDownload: false,
showSign: true,
...CONTRACT_DETAIL_BASE,
contractStatus: '待签署',
},
{
id: '3',
contractNo: 'C8K2M202408001',
contractNo: CONTRACT_DETAIL_BASE.contractNo,
title: '社区康养智慧系统',
type: '软件开发',
relatedLabel: '关联项目',
@@ -49,5 +65,10 @@ export const CONTRACT_LIST: ContractItem[] = [
],
showDownload: false,
showSign: false,
...CONTRACT_DETAIL_BASE,
},
]
export function getContract(id: string): ContractItem | null {
return CONTRACT_LIST.find(item => item.id === id) || null
}

View File

@@ -11,7 +11,7 @@ import iconInvoice from '@/static/icon/invoice.png'
import iconMessagePush from '@/static/icon/message-push.png'
import iconQualification from '@/static/icon/qualification.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 = {
nickname: '这里是昵称',

View File

@@ -223,6 +223,24 @@
"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",
"style": {

View 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;
}

View 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>

View File

@@ -34,7 +34,9 @@ function badgeClass(type: ContractItem['badges'][number]['type']) {
}
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) {
@@ -46,7 +48,9 @@ function handleDownload(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>

View 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;
}

View 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>

View 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;
}

View 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>

View 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;
}

View 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>

View File

@@ -61,6 +61,10 @@ function handleQuickEntryTap(entry: QuickEntryItem) {
navigateQuickEntry(entry.id, entry.label)
}
function handleViewAllProjects() {
navigateQuickEntry('project', '项目中心')
}
const projects: ProjectItem[] = [
{
id: '1',
@@ -187,7 +191,10 @@ const projects: ProjectItem[] = [
<text class="home__section-title">
在办项目
</text>
<text class="home__section-action">
<text
class="home__section-action"
@tap="handleViewAllProjects"
>
查看全部
</text>
</view>

View File

@@ -20,4 +20,13 @@ export interface ContractItem {
badges: ContractBadge[]
showDownload: boolean
showSign: boolean
contractName: string
contractType: string
signDate: string
effectiveDate: string
expiryDate: string
relatedProject: string
contractStatus: string
projectId: string
fileName: string
}