chore: initial import

This commit is contained in:
liangjian
2026-06-29 09:42:53 +08:00
commit 9fea127de1
241 changed files with 82767 additions and 0 deletions

View File

@@ -0,0 +1,376 @@
<template>
<view class="contract-detail-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
</tn-nav-bar>
<view class="page-main" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view class="overview-panel">
<view class="overview-header">
<view>
<view class="overview-title">{{ t('overviewTitle') }}</view>
<view class="overview-subtitle">{{ t('contractNoLabel') }}{{ contractNo || t('notAvailable') }}</view>
</view>
<view class="overview-badge">
<text class="tn-icon-read"></text>
<text>{{ Math.round(scale * 100) }}%</text>
</view>
</view>
<view class="overview-tips">
<view class="tip-chip">
<text class="tn-icon-safe-fill"></text>
<text>{{ t('signedChip') }}</text>
</view>
<view class="tip-chip">
<text class="tn-icon-charts-ring"></text>
<text>{{ t('zoomSupportedChip') }}</text>
</view>
<view class="tip-chip" v-if="imageUrl">
<text class="tn-icon-image-text-fill"></text>
<text>{{ t('imageModeChip') }}</text>
</view>
<view class="tip-chip" v-else-if="contractContent">
<text class="tn-icon-text-new"></text>
<text>{{ t('textModeChip') }}</text>
</view>
</view>
</view>
<view v-if="loading" class="empty-panel">
<view class="empty-title">{{ t('loadingTitle') }}</view>
<view class="empty-desc">{{ t('loadingDesc') }}</view>
</view>
<view v-else-if="imageUrl || contractContent" class="preview-panel">
<scroll-view scroll-y scroll-x class="scroll-area" :enable-flex="true">
<view class="a4-wrapper" :style="{ transform: `scale(${scale})`, transformOrigin: 'top center' }">
<view class="a4-paper">
<image
v-if="imageUrl"
class="contract-image"
:src="imageUrl"
mode="widthFix"
@click="previewImage"
></image>
<rich-text v-else class="contract-rich-text" :nodes="contractContent"></rich-text>
</view>
</view>
</scroll-view>
<view class="zoom-controls">
<view class="zoom-btn" @click="zoomOut">
<text class="tn-icon-reduce-circle"></text>
</view>
<view class="zoom-text">{{ Math.round(scale * 100) }}%</view>
<view class="zoom-btn" @click="zoomIn">
<text class="tn-icon-add-circle"></text>
</view>
</view>
</view>
<view v-else class="empty-panel">
<view class="empty-icon">
<text class="tn-icon-folder"></text>
</view>
<view class="empty-title">{{ t('emptyTitle') }}</view>
<view class="empty-desc">{{ t('emptyDesc') }}</view>
</view>
</view>
</view>
</template>
<script>
import { addFilePrefix } from '@/utils/common.js'
import { contractPreview } from '@/api/index.js'
import { normalizeRichTextImages } from '../../utils/rich-text'
function resolvePreviewPayload(response) {
const payload = response && response.data ? response.data : response
return {
content: (payload && payload.content) || (payload && payload.data && payload.data.content) || '',
imageUrl: (payload && payload.contentImageUrl) || (payload && payload.data && payload.data.contentImageUrl) || ''
}
}
function normalizeRichTextContent(content = '') {
return normalizeRichTextImages(
content,
'display:block;width:100%;max-width:100%;height:auto;box-sizing:border-box;border-radius:12rpx;margin:16rpx 0;vertical-align:top;'
)
}
export default {
i18n: {
zh: {
navTitle: '合同详情',
overviewTitle: '合同影像',
contractNoLabel: '合同编号:',
notAvailable: '未获取',
signedChip: '已签合同',
zoomSupportedChip: '支持缩放',
imageModeChip: 'A4 影像模式',
textModeChip: '正文预览模式',
loadingTitle: '合同加载中',
loadingDesc: '正在获取合同影像与正文,请稍候。',
emptyTitle: '暂无合同内容',
emptyDesc: '页面未带入影像地址,且通过合同编号也未获取到可预览的合同内容。'
},
en: {
navTitle: 'Contract Detail',
overviewTitle: 'Contract Preview',
contractNoLabel: 'Contract No.: ',
notAvailable: 'N/A',
signedChip: 'Signed Contract',
zoomSupportedChip: 'Zoom Supported',
imageModeChip: 'A4 Image Mode',
textModeChip: 'Text Preview Mode',
loadingTitle: 'Loading contract',
loadingDesc: 'Fetching contract images and content. Please wait.',
emptyTitle: 'No contract content',
emptyDesc: 'No previewable contract content was provided by image URL or contract number.'
}
},
data() {
return {
contractNo: '',
orderNo: '',
loading: false,
imageUrl: '',
contractContent: '',
scale: 1
}
},
onLoad(options = {}) {
this.contractNo = decodeURIComponent(options.no || '')
this.orderNo = decodeURIComponent(options.orderNo || '')
this.imageUrl = addFilePrefix(decodeURIComponent(options.contentImageUrl || ''))
this.fetchContractDetail()
},
methods: {
t(key, params) {
return this.$tm('pkgService.contract_detail.contract_detail', key, params)
},
fetchContractDetail() {
if (!this.contractNo || (this.imageUrl && this.contractContent)) {
return Promise.resolve()
}
this.loading = true
this.scale = 1
return contractPreview({ contractNo: this.contractNo })
.then((response) => {
const payload = resolvePreviewPayload(response)
this.contractContent = normalizeRichTextContent(payload.content || '')
if (!this.imageUrl) {
this.imageUrl = addFilePrefix(payload.imageUrl || '')
}
})
.catch((error) => {
console.error('fetch contract detail failed', error)
this.contractContent = ''
})
.finally(() => {
this.loading = false
})
},
previewImage() {
if (!this.imageUrl) {
return
}
uni.previewImage({
urls: [this.imageUrl],
current: this.imageUrl
})
},
zoomIn() {
if (this.scale < 3) {
this.scale = Number((this.scale + 0.2).toFixed(1))
}
},
zoomOut() {
if (this.scale > 0.6) {
this.scale = Number((this.scale - 0.2).toFixed(1))
}
}
}
}
</script>
<style scoped lang="scss">
.contract-detail-page {
min-height: 100vh;
background: linear-gradient(180deg, #f0f6ff 0%, #f8faff 100%);
}
.page-main {
padding: 0 24rpx calc(40rpx + env(safe-area-inset-bottom));
}
.overview-panel,
.preview-panel,
.empty-panel {
background: rgba(255, 255, 255, 0.96);
border-radius: 28rpx;
border: 1px solid rgba($brand-primary, 0.08);
box-shadow: 0 18rpx 48rpx rgba(25, 60, 104, 0.1);
}
.overview-panel {
padding: 28rpx;
}
.overview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.overview-title {
font-size: 34rpx;
font-weight: 700;
color: #1f2d3d;
}
.overview-subtitle {
margin-top: 8rpx;
font-size: 22rpx;
color: #7c8da0;
}
.overview-badge {
display: inline-flex;
align-items: center;
gap: 10rpx;
padding: 10rpx 20rpx;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 700;
color: $brand-primary;
background: rgba($brand-primary, 0.1);
}
.overview-tips {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
}
.tip-chip {
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 10rpx 18rpx;
border-radius: 999rpx;
font-size: 22rpx;
color: #61758a;
background: #f7fafd;
border: 1px solid rgba($brand-primary, 0.08);
}
.preview-panel {
position: relative;
overflow: hidden;
margin-top: 24rpx;
min-height: 980rpx;
background: linear-gradient(180deg, #e8eef5 0%, #e0e8f1 100%);
}
.scroll-area {
width: 100%;
height: 980rpx;
}
.a4-wrapper {
padding: 40rpx 24rpx 120rpx;
display: flex;
justify-content: center;
transition: transform 0.2s ease-out;
}
.a4-paper {
width: 650rpx;
min-height: 900rpx;
padding: 24rpx;
box-sizing: border-box;
background: #ffffff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 16rpx 42rpx rgba(28, 53, 90, 0.1);
}
.contract-image {
display: block;
width: 100%;
}
.contract-rich-text {
display: block;
width: 100%;
color: #334155;
word-break: break-all;
}
.zoom-controls {
position: absolute;
right: 32rpx;
bottom: 32rpx;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.96);
border-radius: 999rpx;
padding: 10rpx 16rpx;
border: 1px solid rgba($brand-primary, 0.08);
}
.zoom-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 34rpx;
color: $brand-primary;
}
.zoom-text {
min-width: 72rpx;
text-align: center;
font-size: 24rpx;
font-weight: 700;
color: #334155;
}
.empty-panel {
margin-top: 24rpx;
padding: 54rpx 32rpx;
text-align: center;
}
.empty-icon {
width: 132rpx;
height: 132rpx;
margin: 0 auto 22rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 70rpx;
color: $brand-primary;
background: rgba($brand-primary, 0.08);
}
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: #233244;
}
.empty-desc {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.7;
color: #8a99a8;
}
</style>