135
packages/app/src/pages/contract/detail.vue
Normal file
135
packages/app/src/pages/contract/detail.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<view class="detail-page" :style="{ paddingTop: navBarHeight + 'px' }">
|
||||
<CustomNavBar :title="previewCardTypeId ? '合同范本' : '合同详情'" show-back />
|
||||
|
||||
<view v-if="state === 'loading'" class="state"><view class="skeleton" /><view class="skeleton tall" /></view>
|
||||
<view v-else-if="state === 'error' || !contractDocument" class="state state-card">
|
||||
<text class="state-title">暂时无法读取合同</text>
|
||||
<text class="state-copy">{{ error }}</text>
|
||||
<button class="secondary" @tap="load">重新加载</button>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view v-if="previewCardTypeId" class="status status--preview">
|
||||
<text class="status__title">合同范本预览</text>
|
||||
<text class="status__copy">支付成功后将生成正式合同,填写姓名并手写签名后生效。金额、日期以支付确认后的正式合同为准。</text>
|
||||
</view>
|
||||
<view v-else-if="detail" class="status" :class="'status--' + detail.status.toLowerCase()">
|
||||
<text class="status__title">{{ CONTRACT_STATUS_LABEL[detail.status] }}</text>
|
||||
<text class="status__copy">{{ statusCopy }}</text>
|
||||
</view>
|
||||
|
||||
<view class="paper">
|
||||
<ContractDocumentView :document="contractDocument" :signature="detail?.signature ?? null" />
|
||||
</view>
|
||||
|
||||
<view v-if="detail && !previewCardTypeId" class="dock">
|
||||
<button v-if="detail.status === MembershipContractStatus.SIGNED" class="primary" :loading="exporting" :disabled="exporting" @tap="exportPdf">导出 PDF</button>
|
||||
<button v-else-if="detail.status === MembershipContractStatus.PENDING_SIGN && !fromAdmin" class="primary" @tap="goSign">去签署</button>
|
||||
<text v-if="detail.status === MembershipContractStatus.SIGNED" class="dock__hint">打开后可通过右上角菜单转发或保存到手机</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { MembershipContractStatus, type ContractDocument, type MembershipContractDetail } from '@mp-pilates/shared'
|
||||
import CustomNavBar from '../../components/CustomNavBar.vue'
|
||||
import ContractDocumentView from '../../components/ContractDocumentView.vue'
|
||||
import {
|
||||
CONTRACT_ROUTES,
|
||||
CONTRACT_STATUS_LABEL,
|
||||
fetchContractDetail,
|
||||
fetchContractTemplatePreview,
|
||||
openContractPdf,
|
||||
} from '../../utils/contract'
|
||||
import { formatDateTimeFull } from '../../utils/format'
|
||||
import { getErrorMessage } from '../../utils/auth'
|
||||
import { getSystemLayout } from '../../utils/system'
|
||||
|
||||
const navBarHeight = getSystemLayout().navBarHeight
|
||||
const contractId = ref('')
|
||||
const previewCardTypeId = ref('')
|
||||
const fromAdmin = ref(false)
|
||||
const detail = ref<MembershipContractDetail | null>(null)
|
||||
const contractDocument = ref<ContractDocument | null>(null)
|
||||
const state = ref<'loading' | 'ready' | 'error'>('loading')
|
||||
const error = ref('')
|
||||
const exporting = ref(false)
|
||||
|
||||
const statusCopy = computed(() => {
|
||||
const current = detail.value
|
||||
if (!current) return ''
|
||||
if (current.status === MembershipContractStatus.SIGNED) return `${current.partyName ?? ''} 于 ${formatDateTimeFull(current.signedAt!)} 签署,合同内容已锁定。`
|
||||
if (current.status === MembershipContractStatus.VOIDED) return current.voidReason || '该合同已作废。'
|
||||
return current.unsignableReason || (current.requireBeforeBooking ? '签署后即可预约课程。' : '请核对会员卡信息并完成补签。')
|
||||
})
|
||||
|
||||
async function load() {
|
||||
state.value = 'loading'
|
||||
error.value = ''
|
||||
try {
|
||||
if (previewCardTypeId.value) {
|
||||
contractDocument.value = (await fetchContractTemplatePreview(previewCardTypeId.value)).document
|
||||
} else {
|
||||
detail.value = await fetchContractDetail(contractId.value)
|
||||
contractDocument.value = detail.value.document
|
||||
}
|
||||
state.value = 'ready'
|
||||
} catch (err) {
|
||||
error.value = getErrorMessage(err, '请检查网络后重试')
|
||||
state.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
async function exportPdf() {
|
||||
if (!detail.value || exporting.value) return
|
||||
exporting.value = true
|
||||
try {
|
||||
await openContractPdf(detail.value.id)
|
||||
} catch (err) {
|
||||
uni.showToast({ title: getErrorMessage(err, '导出失败,请重试'), icon: 'none' })
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goSign() {
|
||||
if (detail.value) uni.redirectTo({ url: `${CONTRACT_ROUTES.SIGN}?id=${detail.value.id}` })
|
||||
}
|
||||
|
||||
onLoad((query) => {
|
||||
contractId.value = String(query?.id || '')
|
||||
previewCardTypeId.value = String(query?.previewCardTypeId || '')
|
||||
fromAdmin.value = query?.from === 'admin'
|
||||
void load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-page { min-height: 100vh; box-sizing: border-box; padding-bottom: calc(220rpx + env(safe-area-inset-bottom)); background: #fbf9f6; color: #514943; }
|
||||
button { font-weight: 400; }
|
||||
button::after { border: 0; }
|
||||
.status { margin: 28rpx 32rpx 0; padding: 26rpx; border-radius: 24rpx; background: #edf1e6; border: 1rpx solid #dfe5d6; }
|
||||
.status__title { display: block; font-size: 30rpx; color: #4f6a5c; }
|
||||
.status__copy { display: block; margin-top: 8rpx; font-size: 23rpx; line-height: 1.8; color: #6f7d68; }
|
||||
.status--pending_sign, .status--preview { background: #f6ece4; border-color: #ecdccf; }
|
||||
.status--pending_sign .status__title, .status--preview .status__title { color: #9c7a6e; }
|
||||
.status--pending_sign .status__copy, .status--preview .status__copy { color: #9a8478; }
|
||||
.status--voided { background: #f0eeeb; border-color: #e5e1dc; }
|
||||
.status--voided .status__title, .status--voided .status__copy { color: #8f8881; }
|
||||
.paper { margin: 24rpx 32rpx 0; padding: 32rpx 28rpx; border-radius: 24rpx; background: #fff; border: 1rpx solid #eee6dd; }
|
||||
.dock { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom)); background: #fbf9f6; border-top: 1rpx solid #ece6de; }
|
||||
.dock__hint { display: block; margin-top: 12rpx; text-align: center; font-size: 21rpx; color: #a39689; }
|
||||
.primary, .secondary { width: 100%; margin: 0; border-radius: 20rpx; line-height: 88rpx; font-size: 28rpx; background: #617d70; color: #fff; }
|
||||
.secondary { background: #eaece3; color: #60745a; }
|
||||
.state { margin: 36rpx 32rpx; }
|
||||
.state-card { margin-top: 90rpx; padding: 50rpx 32rpx; background: #f4f1ec; border-radius: 28rpx; text-align: center; }
|
||||
.state-title { display: block; font-family: 'Songti SC', 'STSong', serif; font-size: 32rpx; color: #635b4f; }
|
||||
.state-copy { display: block; margin: 18rpx 0 30rpx; font-size: 24rpx; line-height: 1.9; color: #8d8271; }
|
||||
.skeleton { height: 220rpx; background: #efebe5; border-radius: 24rpx; margin-bottom: 24rpx; animation: breathe 1.2s infinite alternate; }
|
||||
.skeleton.tall { height: 520rpx; }
|
||||
@keyframes breathe { to { opacity: .45 } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user