Files
mp-pilates/packages/app/src/pages/contract/detail.vue

136 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>