feat: 同步签约宝手机端功能到签约宝页面

- sign页面与签约宝精简版对齐:查看合同入口、AI积分区块(合同审查/我的报告/积分充值)
- 新增pages/ai/{points,review,reports}三个页面(与签约宝精简版一致)
- pages.json注册AI页面路由
This commit is contained in:
2026-10-23 19:57:27 +08:00
parent e134209d16
commit c4ca8a87fa
1301 changed files with 314434 additions and 207 deletions
+243
View File
@@ -0,0 +1,243 @@
<template>
<view class="page-wrap">
<uititle title="积分管理"></uititle>
<!-- 积分概览 -->
<view class="points-header">
<view class="ph-card main">
<view class="ph-label">可用积分</view>
<view class="ph-value">{{ pointsBalance }}</view>
</view>
<view class="ph-card sub">
<view class="ph-label">累计获得</view>
<view class="ph-value">{{ pointsTotalEarned }}</view>
</view>
</view>
<!-- 充值套餐 -->
<view class="section-title">💎 积分充值</view>
<view class="recharge-grid">
<view v-for="(pkg, i) in rechargePackages" :key="i"
:class="['recharge-card', selectedRecharge === i && 'selected']"
@click="selectedRecharge = i">
<view class="rc-points">{{ pkg.points }}</view>
<view class="rc-price">{{ pkg.price }}</view>
<view v-if="pkg.bonus" class="rc-bonus">{{ pkg.bonus }}</view>
</view>
</view>
<view class="p-20">
<u-button type="primary" shape="circle" @click="doRecharge">
立即充值 {{ rechargePackages[selectedRecharge].price }}({{ rechargePackages[selectedRecharge].points }} 积分)
</u-button>
</view>
<!-- 积分明细 -->
<view class="section-title">📊 积分明细</view>
<view v-if="pointsLogList.length === 0" class="empty-tip">暂无积分记录</view>
<view v-for="(item, i) in pointsLogList" :key="i" class="log-item">
<view class="log-left">
<view class="log-reason">{{ item.reason }}</view>
<view class="log-time">{{ item._add_time_str }}</view>
</view>
<view :class="['log-amount', item.type === 'earn' ? 'green' : 'red']">
{{ item.type === 'earn' ? '+' : '-' }}{{ Math.abs(item.amount) }}
</view>
</view>
</view>
</template>
<script>
import uititle from '@/components/common/title'
import dayjs from 'dayjs'
export default {
components: { uititle },
data() {
return {
pointsBalance: 0,
pointsTotalEarned: 0,
pointsLogList: [],
selectedRecharge: 1,
rechargePackages: [
{ points: 10000, price: '20元', bonus: '' },
{ points: 50000, price: '100元', bonus: '' },
{ points: 100000, price: '200元', bonus: '' },
{ points: 450000, price: '900元', bonus: '' },
{ points: 1000000, price: '2000元', bonus: '' },
{ points: 2000000, price: '4000元', bonus: '' },
]
}
},
onShow() {
this.getPointsBalance()
this.getPointsLog()
},
methods: {
async getPointsBalance() {
const res = await vk.callFunction({ url: 'client/points/kh/getBalance' })
if (res.code === 0 && res.data) {
this.pointsBalance = res.data.balance || 0
this.pointsTotalEarned = res.data.total_earned || 0
}
},
async getPointsLog() {
const res = await vk.callFunction({
url: 'client/points/kh/getLog',
data: { pageIndex: 1, pageSize: 50 }
})
if (res.code === 0 && res.data) {
this.pointsLogList = (res.data.rows || []).map(r => ({
...r,
_add_time_str: dayjs(r._add_time && r._add_time['$date'] ? r._add_time['$date'] : r._add_time).format('YYYY-MM-DD HH:mm')
}))
}
},
async doRecharge() {
const pkg = this.rechargePackages[this.selectedRecharge]
const priceMap = {
'20元': 2000, '100元': 10000, '200元': 20000,
'900元': 90000, '2000元': 200000, '4000元': 400000
}
const price = priceMap[pkg.price] || 2000
const res = await vk.callFunction({
url: 'client/points/kh/createOrder',
data: { points: pkg.points, price }
})
if (res.code === 0 && res.data) {
uni.navigateTo({ url: '/pages/shop/pay?id=' + res.data })
} else {
uni.showToast({ title: res.msg || '创建订单失败', icon: 'none' })
}
}
}
}
</script>
<style lang="scss" scoped>
.page-wrap {
min-height: 100vh;
background: #f5f5f5;
}
.points-header {
display: flex;
gap: 20rpx;
padding: 30rpx;
}
.ph-card {
flex: 1;
padding: 30rpx;
border-radius: 16rpx;
color: #fff;
text-align: center;
}
.ph-card.main {
background: linear-gradient(135deg, #a92924, #d4574e);
}
.ph-card.sub {
background: linear-gradient(135deg, #333, #666);
}
.ph-label {
font-size: 24rpx;
opacity: 0.85;
}
.ph-value {
font-size: 48rpx;
font-weight: 700;
margin-top: 8rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
padding: 20rpx 30rpx 10rpx;
}
.recharge-grid {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
padding: 0 30rpx;
}
.recharge-card {
flex: 0 0 calc(33.33% - 12rpx);
padding: 24rpx 0;
text-align: center;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
background: #fff;
position: relative;
}
.recharge-card.selected {
border-color: #a92924;
background: #fdf0f0;
}
.rc-points {
font-size: 32rpx;
font-weight: 700;
color: #333;
}
.rc-price {
font-size: 24rpx;
color: #999;
margin-top: 4rpx;
}
.rc-bonus {
position: absolute;
top: -2rpx;
right: -2rpx;
background: #a92924;
color: #fff;
font-size: 18rpx;
padding: 2rpx 10rpx;
border-radius: 0 10rpx 0 10rpx;
}
.log-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.log-left {
flex: 1;
}
.log-reason {
font-size: 28rpx;
color: #333;
}
.log-time {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.log-amount {
font-size: 32rpx;
font-weight: 600;
}
.log-amount.green { color: #67c23a; }
.log-amount.red { color: #f56c6c; }
.empty-tip {
text-align: center;
padding: 60rpx 0;
color: #999;
font-size: 26rpx;
}
</style>
+361
View File
@@ -0,0 +1,361 @@
<template>
<view class="page-wrap">
<uititle title="我的报告"></uititle>
<!-- 查询按钮 -->
<view class="action-bar">
<u-button type="primary" size="small" shape="circle" @click="queryReports">
🔍 查询报告
</u-button>
</view>
<!-- 空状态 -->
<view v-if="reportList.length === 0 && !loading" class="empty-box">
<view class="empty-icon">📋</view>
<view class="empty-text">暂无审查报告</view>
<view class="empty-hint">点击上方「查询报告」从服务器加载</view>
</view>
<!-- 加载中 -->
<view v-if="loading" class="loading-box">
<u-loading-icon size="48"></u-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<!-- 报告列表 -->
<view v-for="(r, i) in reportList" :key="i" class="report-item">
<view class="report-header">
<view class="report-name">📄 {{ r.fileName }}</view>
<view :class="['status-tag', 'status-' + r.status]">{{ statusMap[r.status] || r.status }}</view>
</view>
<view class="report-meta">{{ r.time }} · 消耗 {{ r.amount }} 积分</view>
<view class="report-actions">
<view v-if="r.htmlUrl" class="action-btn" @click="openUrl(r.htmlUrl)">📄 报告下载</view>
<view v-if="r.docxUrl" class="action-btn" @click="openUrl(r.docxUrl)">📝 下载 Word</view>
<view v-if="r.fileUrl" class="action-btn" @click="openUrl(r.fileUrl)">📎 查看上传文件</view>
<!-- 报告均可下载查看,注释在线查看功能
<view v-if="r.status === 'completed' && r.result" class="action-btn" @click="viewSummary(r)">📋 在线查看报告</view>
-->
<view v-if="r.status === 'uploaded' && !r._submitting" class="action-btn action-btn-primary" @click="reReview(r)">🔁 重新审查</view>
<view v-if="r._submitting" class="action-btn action-btn-disabled">提交中...</view>
</view>
</view>
<!-- 摘要弹窗 -->
<u-popup :show="showSummary" mode="bottom" @close="showSummary = false" round="24">
<view class="summary-popup">
<view class="summary-title">📋 审查摘要</view>
<scroll-view scroll-y class="summary-body">
<rich-text :nodes="summaryContent"></rich-text>
</scroll-view>
<view class="summary-actions">
<u-button v-if="summaryHtmlUrl" type="primary" size="small" @click="openUrl(summaryHtmlUrl)">查看完整报告</u-button>
<u-button size="small" @click="showSummary = false">关闭</u-button>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import uititle from '@/components/common/title'
import dayjs from 'dayjs'
export default {
components: { uititle },
data() {
return {
reportList: [],
loading: false,
showSummary: false,
summaryContent: '',
summaryHtmlUrl: '',
statusMap: {
uploaded: '待审查',
reviewing: '排队中',
processing: '审查中',
completed: '已完成',
failed: '失败'
}
}
},
onShow() {
this.queryReports()
},
methods: {
// 获取当前登录用户uid(vuex优先,storage兜底)
getUid() {
let uid = (typeof vk.getVuex === 'function' && vk.getVuex('$user.userInfo._id')) || ''
if (!uid) {
const cache = vk.getStorageSync('uni_id_user_info') || {}
uid = cache._id || cache.uid || ''
}
return uid
},
// 未登录引导
checkLogin() {
if (this.getUid()) return true
uni.showModal({
title: '提示',
content: '请先登录后查看报告',
confirmText: '去登录',
success: (res) => {
if (res.confirm) uni.navigateTo({ url: '/pages/login/login' })
}
})
return false
},
async queryReports() {
if (!this.checkLogin()) return
this.loading = true
try {
const res = await vk.callFunction({
url: 'client/ai/pub/getMyReports',
data: { pageIndex: 1, pageSize: 50, uid: this.getUid() }
})
if (res.code === 0 && res.data && res.data.rows) {
this.reportList = res.data.rows.map(r => ({
fileName: r.file_name,
fileSize: r.file_size,
fileId: r.file_id || '',
fileUrl: r.file_url || '',
htmlUrl: r.html_url || '',
docxUrl: r.docx_url || '',
time: dayjs(r._add_time && r._add_time['$date'] ? r._add_time['$date'] : r._add_time).format('YYYY-MM-DD HH:mm'),
amount: r.points_used || 0,
result: r.result,
status: r.status,
recordId: r._id
}))
}
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
this.loading = false
},
viewSummary(r) {
this.summaryContent = this.formatContent(r.result)
this.summaryHtmlUrl = r.htmlUrl
this.showSummary = true
},
// 待审查记录 → 重新发起审查
async reReview(r) {
if (!this.checkLogin()) return
if (!r.fileId) {
uni.showToast({ title: '该记录缺少文件信息', icon: 'none' })
return
}
const confirmed = await new Promise((resolve) => {
uni.showModal({
title: '重新审查',
content: `确认对「${r.fileName}」发起审查?审查完成后将按文件大小扣除相应积分。`,
confirmText: '确认审查',
confirmColor: '#a92924',
success: (res) => resolve(!!res.confirm),
fail: () => resolve(false)
})
})
if (!confirmed) return
uni.showLoading({ title: '提交中...', mask: true })
try {
const balanceRes = await vk.callFunction({ url: 'client/points/kh/getBalance' })
if (balanceRes.code !== 0 || !balanceRes.data || balanceRes.data.balance < 200) {
uni.hideLoading()
uni.showToast({ title: '积分不足,请先充值', icon: 'none' })
return
}
this.$set(r, '_submitting', true)
const res = await vk.callFunction({
url: 'client/ai/pub/review',
data: {
record_id: r.recordId,
file_id: r.fileId,
file_name: r.fileName,
file_size: r.fileSize || 0,
uid: this.getUid()
}
})
uni.hideLoading()
this.$set(r, '_submitting', false)
if (res.code === 0) {
r.status = (res.data && res.data.status) || 'reviewing'
uni.showToast({ title: '已提交审查', icon: 'success' })
setTimeout(() => this.queryReports(), 1500)
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
this.$set(r, '_submitting', false)
uni.showToast({ title: '提交异常', icon: 'none' })
}
},
formatContent(text) {
if (!text) return '<p>暂无审查结果</p>'
let html = text
html = html.replace(/^###\s+(.+)$/gm, '<h4 style="font-size:15px;margin:12px 0 6px;">$1</h4>')
html = html.replace(/^##\s+(.+)$/gm, '<h3 style="font-size:16px;margin:16px 0 8px;">$1</h3>')
html = html.replace(/\*\*(.*?)\*\*/g, '<strong style="color:#a92924;">$1</strong>')
html = html.replace(/^[\-\•]\s+(.+)$/gm, '<div style="padding-left:12px;">• $1</div>')
html = html.replace(/^(\d+)\.\s+(.+)$/gm, '<div style="padding-left:12px;">$1. $2</div>')
html = html.replace(/\n/g, '<br>')
return html
},
openUrl(url) {
if (url) {
// #ifdef H5
window.open(url, '_blank')
// #endif
// #ifndef H5
uni.navigateTo({ url: '/pages/ai/webview?url=' + encodeURIComponent(url) })
// #endif
}
}
}
}
</script>
<style lang="scss" scoped>
.page-wrap {
min-height: 100vh;
background: #f5f5f5;
}
.action-bar {
padding: 20rpx 30rpx;
display: flex;
justify-content: flex-end;
}
.empty-box {
text-align: center;
padding: 120rpx 30rpx;
}
.empty-icon {
font-size: 80rpx;
}
.empty-text {
font-size: 30rpx;
font-weight: 600;
margin-top: 16rpx;
}
.empty-hint {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.loading-box {
text-align: center;
padding: 60rpx 0;
}
.loading-text {
display: block;
font-size: 28rpx;
margin-top: 16rpx;
}
.report-item {
background: #fff;
margin: 16rpx 30rpx;
padding: 24rpx;
border-radius: 12rpx;
}
.report-header {
display: flex;
align-items: center;
gap: 16rpx;
}
.report-name {
flex: 1;
font-size: 28rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-tag {
font-size: 22rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
.status-uploaded { background: #f0f0f0; color: #999; }
.status-reviewing { background: #fdf6ec; color: #e6a23c; }
.status-processing { background: #fdf6ec; color: #e6a23c; }
.status-completed { background: #f0f9eb; color: #67c23a; }
.status-failed { background: #fef0f0; color: #f56c6c; }
.report-meta {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.report-actions {
display: flex;
gap: 16rpx;
margin-top: 16rpx;
flex-wrap: wrap;
}
.action-btn {
font-size: 24rpx;
color: #a92924;
padding: 8rpx 20rpx;
border: 1rpx solid #a92924;
border-radius: 8rpx;
}
.action-btn:active {
background: #a92924;
color: #fff;
}
.action-btn-primary {
background: #a92924;
color: #fff;
}
.action-btn-primary:active {
background: #8c1f1b;
}
.action-btn-disabled {
color: #bbb;
border-color: #ddd;
}
.summary-popup {
padding: 30rpx;
max-height: 70vh;
}
.summary-title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 20rpx;
}
.summary-body {
max-height: 50vh;
font-size: 26rpx;
line-height: 1.8;
}
.summary-actions {
display: flex;
gap: 16rpx;
margin-top: 24rpx;
justify-content: center;
}
</style>
+369
View File
@@ -0,0 +1,369 @@
<template>
<view class="page-wrap">
<uititle title="AI 合同审查"></uititle>
<!-- 积分提示 -->
<view class="notice-bar">
<text>💎 当前积分:{{ pointsBalance }}|每 100KB 约 200 积分</text>
</view>
<!-- 免责声明 -->
<view class="disclaimer-bar">
<text>⚠️ 请确保合同内容清晰可见,如因不清晰导致分析失败,积分正常消耗。</text>
</view>
<!-- 上传区 -->
<view v-if="!reviewFile" class="upload-area" @click="selectReviewFile">
<view class="upload-icon">📤</view>
<view class="upload-text">上传合同文件</view>
<view class="upload-hint">支持 PDF / Word(.doc .docx),最大 20MB</view>
</view>
<!-- 上传中 -->
<view v-if="reviewUploading" class="loading-box">
<u-loading-icon size="48"></u-loading-icon>
<text class="loading-text">正在上传文件到审查系统...</text>
</view>
<!-- 文件信息 + 操作 -->
<view v-if="reviewFile && !reviewUploading" class="file-section">
<view class="file-card">
<view class="file-name">📄 {{ reviewFile.name }}</view>
<view class="file-size">{{ formatFileSize(reviewFile.size) }}</view>
</view>
<view class="review-mode">
<view class="mode-item selected">
<view class="mode-icon">🔍</view>
<view class="mode-info">
<view class="mode-title">标准审查</view>
<view class="mode-desc">风险条款识别 · 合规性检查</view>
</view>
</view>
</view>
<view class="p-20">
<u-button type="primary" shape="circle" :loading="reviewLoading"
:disabled="pointsBalance < reviewPoints + 200"
@click="startReview">
{{ reviewLoading ? '审查中...' : '开始审查(约 ' + reviewPoints + ' 积分)' }}
</u-button>
<view v-if="pointsBalance < reviewPoints + 200" class="warn-text">
积分不足,当前余额 {{ pointsBalance }},需要 {{ reviewPoints }} 积分且审查后余额需保留至少 200 积分
</view>
</view>
</view>
<!-- 审查中 -->
<view v-if="reviewLoading" class="loading-box">
<u-loading-icon size="48"></u-loading-icon>
<text class="loading-text">🤖 AI 正在分析合同,请稍候...</text>
<text class="loading-hint">预计 3-5 分钟,可前往「我的报告」查看进度</text>
</view>
</view>
</template>
<script>
import uititle from '@/components/common/title'
export default {
components: { uititle },
data() {
return {
pointsBalance: 0,
reviewFile: null,
reviewFileId: null,
reviewRecordId: null,
reviewUploading: false,
reviewLoading: false,
reviewPoints: 200,
reviewProgress: 0
}
},
onShow() {
this.getPointsBalance()
},
methods: {
// 获取当前登录用户uid(vuex优先,storage兜底)
getUid() {
let uid = (typeof vk.getVuex === 'function' && vk.getVuex('$user.userInfo._id')) || ''
if (!uid) {
const cache = vk.getStorageSync('uni_id_user_info') || {}
uid = cache._id || cache.uid || ''
}
return uid
},
async getPointsBalance() {
const res = await vk.callFunction({ url: 'client/points/kh/getBalance' })
if (res.code === 0 && res.data) {
this.pointsBalance = res.data.balance || 0
}
},
selectReviewFile() {
uni.chooseFile({
count: 1,
type: 'file',
extension: ['.pdf', '.doc', '.docx'],
success: (res) => {
const filePath = res.tempFilePaths[0]
const fileName = res.tempFiles[0].name || filePath.split('/').pop()
const fileSize = res.tempFiles[0].size
if (fileSize > 20 * 1024 * 1024) {
uni.showToast({ title: '文件大小超过 20MB', icon: 'none' })
return
}
this.reviewFile = { name: fileName, size: fileSize, path: filePath }
this.reviewFileId = null
this.reviewPoints = Math.max(200, Math.ceil(fileSize / (100 * 1024)) * 200)
this.uploadFile(filePath, fileName, fileSize)
}
})
},
async uploadFile(filePath, fileName, fileSize) {
this.reviewUploading = true
try {
const base64 = await this.fileToBase64(filePath)
const res = await vk.callFunction({
url: 'client/ai/pub/review',
data: {
file_base64: base64,
file_name: fileName,
file_size: fileSize,
uid: this.getUid(),
upload_only: true
}
})
if (res.code === 0 && res.data && res.data.file_id) {
this.reviewFileId = res.data.file_id
this.reviewRecordId = res.data.record_id
uni.showToast({ title: '文件上传成功', icon: 'success' })
} else {
uni.showToast({ title: res.msg || '上传失败', icon: 'none' })
this.reviewFile = null
}
} catch (e) {
uni.showToast({ title: '上传异常', icon: 'none' })
this.reviewFile = null
}
this.reviewUploading = false
},
fileToBase64(filePath) {
return new Promise((resolve, reject) => {
// #ifdef H5
const xhr = new XMLHttpRequest()
xhr.open('GET', filePath, true)
xhr.responseType = 'blob'
xhr.onload = () => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result.split(',')[1])
reader.onerror = reject
reader.readAsDataURL(xhr.response)
}
xhr.onerror = reject
xhr.send()
// #endif
// #ifndef H5
uni.getFileSystemManager().readFile({
filePath,
encoding: 'base64',
success: (res) => resolve(res.data),
fail: reject
})
// #endif
})
},
async startReview() {
if (!this.reviewFileId || !this.reviewRecordId) {
uni.showToast({ title: '请先上传文件', icon: 'none' })
return
}
// 积分余额检查
const balanceRes = await vk.callFunction({ url: 'client/points/kh/getBalance' })
if (balanceRes.code !== 0 || !balanceRes.data || balanceRes.data.balance < 200) {
uni.showToast({ title: '积分不足,请先充值', icon: 'none' })
return
}
this.reviewLoading = true
try {
const res = await vk.callFunction({
url: 'client/ai/pub/review',
data: {
record_id: this.reviewRecordId,
file_id: this.reviewFileId,
file_name: this.reviewFile.name,
file_size: this.reviewFile.size
}
})
if (res.code === 0) {
uni.showModal({
title: '审查已提交',
content: 'AI 正在分析中,预计 3-5 分钟完成。您可以在「我的报告」中查看结果。',
showCancel: false,
confirmText: '知道了',
success: () => {
this.resetReview()
uni.navigateBack()
}
})
} else {
uni.showToast({ title: res.msg || '发起审查失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '请求异常', icon: 'none' })
}
this.reviewLoading = false
},
resetReview() {
this.reviewFile = null
this.reviewFileId = null
this.reviewRecordId = null
this.reviewUploading = false
this.reviewLoading = false
this.reviewProgress = 0
},
formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
}
}
</script>
<style lang="scss" scoped>
.page-wrap {
min-height: 100vh;
background: #f5f5f5;
}
.notice-bar {
background: #fff8e1;
padding: 20rpx 30rpx;
font-size: 24rpx;
color: #b8860b;
}
.disclaimer-bar {
background: #fef0f0;
padding: 20rpx 30rpx;
font-size: 22rpx;
color: #f56c6c;
}
.upload-area {
margin: 30rpx;
padding: 80rpx 30rpx;
border: 4rpx dashed #d0d0d0;
border-radius: 16rpx;
text-align: center;
background: #fff;
}
.upload-area:active {
border-color: #a92924;
background: #fdf0f0;
}
.upload-icon {
font-size: 80rpx;
}
.upload-text {
font-size: 32rpx;
font-weight: 600;
margin-top: 16rpx;
}
.upload-hint {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.loading-box {
text-align: center;
padding: 60rpx 30rpx;
}
.loading-text {
display: block;
font-size: 28rpx;
margin-top: 16rpx;
}
.loading-hint {
display: block;
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.file-section {
margin: 30rpx;
}
.file-card {
background: #f9f9f9;
padding: 24rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
gap: 16rpx;
}
.file-name {
flex: 1;
font-size: 28rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
font-size: 24rpx;
color: #999;
flex-shrink: 0;
}
.review-mode {
margin-top: 24rpx;
}
.mode-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx;
border: 2rpx solid #a92924;
border-radius: 12rpx;
background: #fdf0f0;
}
.mode-icon {
font-size: 40rpx;
}
.mode-title {
font-size: 28rpx;
font-weight: 600;
}
.mode-desc {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.warn-text {
text-align: center;
font-size: 24rpx;
color: #f56c6c;
margin-top: 16rpx;
}
</style>
+275 -207
View File
@@ -3,24 +3,44 @@
<!-- 吸顶 -->
<u-sticky offset-top="0" customNavHeight="0">
<!-- 顶部 -->
<uititle title="金久信签约宝"></uititle>
<uititle title="金久信签约宝" :show-back="false"></uititle>
</u-sticky>
<view class="p-20">
<common-swiper page="qyb"/>
<view class="my-15 ">
<u-button shape="circle" type="primary" @click="toContract">查看合同</u-button>
</view>
<view class="layout layout-publish m-top-20 bg-white p-20">
<view class="pb-30">
<text class="text-main">您剩余金久信签约宝次数: {{ userInfo.qybNum }}次</text>
<view class="qyb-action-row flex justify-start items-center mt-20">
<view class="qyb-buy-action">
<u-button size="mini" type="primary" @click.native="dialogVisible = true"
<u-button size="max" type="primary" @click.native="dialogVisible = true"
:custom-style="{padding: 0, width: '100%'}">立即购买</u-button>
</view>
<view class="qyb-log-action ml-25">
<u-button size="mini" type="success" @click.native="openLog"
<u-button size="max" type="success" @click.native="openLog"
:custom-style="{padding: 0, width: '100%'}">明细</u-button>
</view>
</view>
</view>
<view class="pb-30">
<text class="text-main">您AI积分剩余: {{ pointsBalance }}积分</text>
<view class="qyb-action-row flex justify-start items-center mt-20" style="display: flex;justify-content:space-between;">
<view class="qyb-buy-action">
<u-button size="max" type="warning" @click="vk.navigateTo('/pages/ai/review')"
:custom-style="{padding: 0, width: '100%'}">合同审查</u-button>
</view>
<view class="qyb-buy-action">
<u-button size="max" type="success" @click="vk.navigateTo('/pages/ai/reports')"
:custom-style="{padding: 0, width: '100%'}">我的报告</u-button>
</view>
<view class="qyb-buy-action">
<u-button size="max" type="primary" @click="vk.navigateTo('/pages/ai/points')"
:custom-style="{padding: 0, width: '100%'}">积分充值</u-button>
</view>
</view>
</view>
<view class="text-main mt-10">温馨提示:尊敬的用户如果您的剩余金久信签约宝次数为0次,请先进行购买,再进行签约。</view>
<view class="card m-20">
<view class="flex justify-between items-center" @click="showSignIntro = !showSignIntro">
@@ -60,7 +80,6 @@
<u-radio activeColor="#a92924" name='2' label="合同签约"></u-radio>
</u-radio-group>
</view>
<!-- <view class="flex justify-start align-center m-top-30">-->
<!-- <view class="lb need m-right-20">交易方式:</view>-->
<!-- <u-radio-group-->
@@ -149,8 +168,8 @@
:previewImage="false"
>
<u-button type="primary" v-if="!formData.files">点击上传</u-button>
<p class="mt-30" @click="previewDoc" v-if="fileName">预览: {{ fileName }}</p>
</u-upload>
<p class="mt-30" @click="previewDoc">预览: {{ fileName }}</p>
</view>
</view>
<view class="group flex justify-start m-top-30 flex-col">
@@ -238,110 +257,106 @@
<u-popup :show="showLog" title="金久信签约宝明细" mode="center" @close="showLog = false" z-index="997" :overlayStyle="{
zIndex: 997
}">
<view style="max-height: 80vh" class="p-20 overflow-y rounded">
<u-tabs :list="[{name: '使用明细'}, {name: '付费明细'}]" :activeStyle="{fontWeight: 'bold', fontSize: '18px'}" :current="logIndex" @click="changeLogTab"></u-tabs>
<view class="flex flex-col justify-center items-center m-20" style="max-height: 60vh;overflow-y: scroll">
<template v-if="logIndex === 0">
<view class="p-15 border-bottom w-100p" v-for="(item, index) in signLog.rows" :key="index">
<view>日期: {{ item._add_time_str }}</view>
<view>项目: {{ parseInt(item.contract.signType) === 1 ? '单据签约' : '合同签约' }}</view>
<view>合同编号: {{ item.contract.transactionId }}</view>
<view>签约身份: {{
item.mineContract ? (item.mineContract.role === 'third' ? '第三方' : (item.mineContract.userId === item.userId ? '发起方' : '接收方')) : ''
}}
</view>
<view>
<view>扣除次数: 1</view>
<view v-if="item.remainNum !== undefined && item.remainNum != null">剩余次数: {{
item.remainNum
}}
</view>
</view>
<u-tabs :list="[{name: '使用明细'}, {name: '付费明细'}]" :activeStyle="{fontWeight: 'bold', fontSize: '18px'}" :current="logIndex" @click="changeLogTab"></u-tabs>
<view class="flex flex-col justify-center items-center m-20" style="max-height: 60vh;overflow-y: scroll">
<template v-if="logIndex === 0">
<view class="p-15 border-bottom w-100p" v-for="(item, index) in signLog.rows" :key="index">
<view>日期: {{ item._add_time_str }}</view>
<view>项目: {{ parseInt(item.contract.signType) === 1 ? '单据签约' : '合同签约' }}</view>
<view>合同编号: {{ item.contract.transactionId }}</view>
<view>签约身份: {{
item.mineContract ? (item.mineContract.role === 'third' ? '第三方' : (item.mineContract.userId === item.userId ? '发起方' : '接收方')) : ''
}}
</view>
<view class="text-center mt-20">
<el-pagination
:page-size="10"
@current-change="getSignLog"
background
layout="prev, pager, next"
:total="signLog.total">
</el-pagination>
<view>
<view>扣除次数: 1</view>
<view v-if="item.remainNum !== undefined && item.remainNum != null">剩余次数: {{ item.remainNum }}</view>
</view>
</template>
<template v-else>
<view class="p-15 border-bottom w-100p" v-for="(item, index) in signLog.rows" :key="index">
<view>日期: {{ item._add_time_str }}</view>
<view>项目: {{ item.type === 12 ? '合同司法调证' : '金久信签约宝续费' }}</view>
<view>流水号: {{ item.orderNo }}</view>
<view>缴费金额: {{ item.type === 12 ? item.amount : item.payAmount }}
</view>
<view>
<view>采购次数: {{ item.goods[0].goodsName }}</view>
<view v-if="item.remainNum">剩余次数: {{ item.remainNum }}</view>
</view>
<view class="flex justify-start items-center">
<text class="mr-15">操作:</text>
<el-button size="mini" @click="openInvoice(item._id)" type="primary"
v-if="!item.isInvoice">申请发票
</view>
<view class="text-center mt-20">
<el-pagination
:page-size="10"
@current-change="getSignLog"
background
layout="prev, pager, next"
:total="signLog.total">
</el-pagination>
</view>
</template>
<template v-else>
<view class="p-15 border-bottom w-100p" v-for="(item, index) in signLog.rows" :key="index">
<view>日期: {{ item._add_time_str }}</view>
<view>项目: {{ item.type === 12 ? '合同司法调证' : '金久信签约宝续费' }}</view>
<view>流水号: {{ item.orderNo }}</view>
<view>缴费金额: {{ item.type === 12 ? item.amount : item.payAmount }}
</view>
<view>
<view>采购次数: {{ item.goods[0].goodsName }}</view>
<view v-if="item.remainNum">剩余次数: {{ item.remainNum }}</view>
</view>
<view class="flex justify-start items-center">
<text class="mr-15">申请发票:</text>
<el-button size="mini" @click="openInvoice(item._id)" type="primary"
v-if="!item.isInvoice">申请发票
</el-button>
<template v-else>
<el-button size="mini" v-if="item.isInvoice === 1">开票中</el-button>
<el-button size="mini" type="success" v-if="item.isInvoice === 2"
@click.native="copyInvoice(item)">下载
</el-button>
<template v-else>
<el-button size="mini" v-if="item.isInvoice === 1">开票中</el-button>
<el-button size="mini" type="success" v-if="item.isInvoice === 2"
@click.native="copyInvoice(item)">下载
</el-button>
</template>
<el-button size="mini" style="margin-left: 10px"
v-if="item.type === 12 && item.certificate && item.certificate.paymentMethod === 'advance'"
@click.native="openPayVoucher(item.certificate)">查看支付凭证
</el-button>
</view>
</template>
<el-button size="mini" style="margin-left: 10px"
v-if="item.type === 12 && item.certificate && item.certificate.paymentMethod === 'advance'"
@click.native="openPayVoucher(item.certificate)">查看支付凭证
</el-button>
</view>
<view class="text-center mt-20">
<el-pagination
:page-size="10"
@current-change="getSignBuyLog"
background
layout="prev, pager, next"
:total="signLog.total">
</el-pagination>
</view>
</template>
</view>
</view>
<view class="text-center mt-20">
<el-pagination
:page-size="10"
@current-change="getSignBuyLog"
background
layout="prev, pager, next"
:total="signLog.total">
</el-pagination>
</view>
</template>
</view>
<el-dialog title="申请发票" :visible.sync="showInvoice" width="80%" top="10px" append-to-body>
<p>请输入开票信息</p>
<u-form ref="invoiceForm" :model="invoiceForm" label-width="140px" labelPosition="top">
<u-form-item label="名称" required>
<u-input v-model="invoiceForm.name" placeholder="请输入企业名称或个人姓名"/>
</u-form-item>
<u-form-item label="税号/证号" required>
<u-input v-model="invoiceForm.code" placeholder="请输入税号或身份证号"/>
</u-form-item>
<u-form-item label="手机号" required>
<u-input v-model="invoiceForm.phone" placeholder="请输入手机号"/>
</u-form-item>
<u-form-item label="电子邮箱" required>
<u-input v-model="invoiceForm.email" placeholder="请输入电子邮箱"/>
</u-form-item>
<u-form-item label="开户行">
<u-input v-model="invoiceForm.bankName" placeholder="请输入开户行"/>
</u-form-item>
<u-form-item label="银行账号">
<u-input v-model="invoiceForm.bankAccount" placeholder="请输入银行账号"/>
</u-form-item>
<u-form-item label="地址" required>
<u-input v-model="invoiceForm.address" placeholder="请输入地址"/>
</u-form-item>
<u-form-item label="单位/个人电话">
<u-input v-model="invoiceForm.tel" placeholder="请输入单位/个人电话"/>
</u-form-item>
</u-form>
<div class="mt-20 flex justify-center items-center">
<el-button type="primary" @click.native="doInvoice">确定</el-button>
<el-button plain @click.native="showInvoice = false">取消</el-button>
</div>
</el-dialog>
</u-popup>
<el-dialog title="申请发票" :visible.sync="showInvoice" width="80%" top="10px" append-to-body>
<p>请输入开票信息</p>
<u-form ref="invoiceForm" :model="invoiceForm" label-width="140px" labelPosition="top">
<u-form-item label="名称" required>
<u-input v-model="invoiceForm.name" placeholder="请输入企业名称或个人姓名"/>
</u-form-item>
<u-form-item label="税号/证号" required>
<u-input v-model="invoiceForm.code" placeholder="请输入税号或身份证号"/>
</u-form-item>
<u-form-item label="手机号" required>
<u-input v-model="invoiceForm.phone" placeholder="请输入手机号"/>
</u-form-item>
<u-form-item label="电子邮箱" required>
<u-input v-model="invoiceForm.email" placeholder="请输入电子邮箱"/>
</u-form-item>
<u-form-item label="开户行">
<u-input v-model="invoiceForm.bankName" placeholder="请输入开户行"/>
</u-form-item>
<u-form-item label="银行账号">
<u-input v-model="invoiceForm.bankAccount" placeholder="请输入银行账号"/>
</u-form-item>
<u-form-item label="地址" required>
<u-input v-model="invoiceForm.address" placeholder="请输入地址"/>
</u-form-item>
<u-form-item label="单位/个人电话">
<u-input v-model="invoiceForm.tel" placeholder="请输入单位/个人电话"/>
</u-form-item>
</u-form>
<div class="mt-20 flex justify-center items-center">
<el-button type="primary" @click.native="doInvoice">确定</el-button>
<el-button plain @click.native="showInvoice = false">取消</el-button>
</div>
</el-dialog>
<el-dialog title="合同预览" :visible.sync="showPdf" width="90%" append-to-body>
<view style="height: 1000rpx">
<u-pdf :url="pdfUrl" v-if="pdfUrl" :showBottom="false" :show-timer="false" :disabled="false" viewHeight="800rpx"
@@ -349,6 +364,10 @@
</view>
</el-dialog>
<collapse></collapse>
<!-- AI 客服悬浮按钮 -->
<view class="ai-float-btn" @click="openAiChat">
<text class="ai-float-icon">🤖</text>
</view>
</view>
</template>
@@ -370,11 +389,11 @@ const SIGN_CONFIRM_CONTENT = `1、您确认,若全体各签约方未完成全
export default {
components: {
Collapse,
uititle,
cmsearch,
cmempty,
uPdf,
Collapse,
},
data() {
return {
@@ -383,12 +402,13 @@ export default {
userInfo: {
qybNum: 0
},
pointsBalance: 0,
pdfUrl: '',
show: false,
showPdf: false,
title: '',
name: '',
companyMobile: '',
pdfUrl: '',
showPdf: false,
formData: {
signType: '1',
tradeMethod: 0,
@@ -398,7 +418,7 @@ export default {
userType: null,
third: null,
signEndDate: null,
contractFile: '',
contractFile: null,
buyerSignKeyword: null,
sellerSignKeyword: null,
otherFile: null,
@@ -408,17 +428,6 @@ export default {
expireTime: '',
minDate: dayjs().valueOf(),
showContactPicker: false,
invoiceForm: {
id: null,
name: null,
code: null,
phone: null,
email: null,
bankName: null,
bankAccount: null,
address: null,
tel: null,
},
userTypeList: [
{
value: 1,
@@ -448,23 +457,33 @@ export default {
qybPackageList: [],
selectedQyb: null,
showSignIntro: false,
showSignFlow: false,
signFlowTitle: '',
showSignTips: false,
signTips: '',
showLog: false,
signIntro: null,
signFlow: null,
signEndDate: null,
invoiceForm: {
id: null,
name: null,
code: null,
phone: null,
email: null,
bankName: null,
bankAccount: null,
address: null,
tel: null,
},
signLog: {},
logIndex: 0,
showInvoice: false,
showLog: false,
showSignFlow: false,
signFlow: null,
signFlowTitle: '',
showSignTips: false,
signTips: '',
}
},
methods: {
dayjs,
afterRead({file}) {
console.log(file)
if (!file) return
// 仅PDF
if (file.type !== 'application/pdf') {
@@ -483,7 +502,7 @@ export default {
}
uniCloud.uploadFile({
filePath: file.url,
cloudPath: dayjs().format('YYYY/MM/DD') + '/' + md5(file.name + Date.now().toString()) + `-${file.name}`,
cloudPath: dayjs().format('YYYY/MM/DD') + '/' + md5(file.name + Date.now().toString()) + '.pdf',
success: ({fileID}) => {
console.log(fileID)
uniCloud.getTempFileURL({
@@ -577,40 +596,38 @@ export default {
} else {
uni.removeStorageSync(`signInfo`)
uni.removeStorageSync(`fileName`)
uni.showModal({
title: '提示',
content: SIGN_CONFIRM_CONTENT,
confirmButtonText: '知悉同意',
showCancel: false,
type: 'warning',
success: async (res) => {
vk.callFunction({
url: 'client/contract/kh/addSign',
title: '请求中...',
data: this.formData,
success: (data) => {
// console.log(data)
vk.navigateTo('/pages/public/contract?tab=1')
},
fail: (err) => {
vk.confirm(err.msg, '提示');
},
});
}
})
// 此处统一使用 SIGN_CONFIRM_CONTENT
// confirmButtonText: '知悉同意',
// showCancelButton: false,
// type: 'warning'
// })
uni.showModal({
title: '提示',
content: SIGN_CONFIRM_CONTENT,
showCancel: false,
confirmText: '知悉同意',
success: res => {
if (res.confirm) {
vk.callFunction({
url: 'client/contract/kh/addSign',
title: '请求中...',
data: this.formData,
success: (data) => {
// console.log(data)
vk.navigateTo('/pages/public/contract?tab=1')
},
fail: (err) => {
vk.confirm(err.msg, '提示');
},
});
}
}
})
}
console.log('complete:', pdfRes)
},
})
},
async doBuyQyb() {
const {data} = await vk.callFunction({
@@ -644,35 +661,21 @@ export default {
})
this.signIntro = res.content
},
async getSignFlow() {
const res = await vk.callFunction({
url: 'client/dict/pub/getDetail',
data: {
id: 'd77d384f686b9bd204bd4dc744cf75d0'
},
})
this.signFlow = res.content
this.signFlowTitle = res.name
},
async getSignTips() {
const res = await vk.callFunction({
url: 'client/dict/pub/getDetail',
data: {
id: '614937966875c18505570c357541c6e0'
},
})
this.signTips = res.content
},
previewDoc() {
// uni.setStorageSync(`signInfo_${this.formData.contractId}`, JSON.stringify(this.formData))
// uni.setStorageSync(`signInfo`, JSON.stringify(this.formData))
// uni.setStorageSync(`fileName`, this.fileName)
// this.pdfUrl = `/hybrid/html/web/viewer.html?file=${this.formData.contractFile}`
// window.location.href = this.formData.contractFile
// window.open(this.formData.contractFile)
if (Array.isArray(this.formData.contractFile)) {
this.pdfUrl = this.formData.contractFile[0]
} else
this.pdfUrl = this.formData.contractFile
this.showPdf = true
},
toContract() {
vk.navigateTo(`/pages/public/contract`)
},
openLog() {
this.showLog = true
this.changeLogTab({index: 0})
@@ -693,6 +696,25 @@ export default {
this.signLog.rows = this.signLog.rows.filter(item => !!item.contract)
this.$forceUpdate()
},
async getSignFlow() {
const res = await vk.callFunction({
url: 'client/dict/pub/getDetail',
data: {
id: 'd77d384f686b9bd204bd4dc744cf75d0'
},
})
this.signFlow = res.content
this.signFlowTitle = res.name
},
async getSignTips() {
const res = await vk.callFunction({
url: 'client/dict/pub/getDetail',
data: {
id: '614937966875c18505570c357541c6e0'
},
})
this.signTips = res.content
},
async getSignBuyLog(pageIndex) {
this.signLog = await vk.callFunction({
url: 'client/order/kh/getList',
@@ -703,7 +725,7 @@ export default {
pageIndex,
pageSize: 10,
signMethod: null,
type: [12, 13]
type: 13
}
})
this.$forceUpdate()
@@ -743,57 +765,84 @@ export default {
// })
window.location.href = row.invoiceUrl
},
getUserInfo() {
vk.userCenter.getCurrentUserInfo({
success: async (data) => {
this.userInfo = data.userInfo
if (this.userInfo.qybNum < 1) {
uni.showModal({
title: '提示',
content: '您的金久信签约宝次数为0, 是否立即购买?',
success: res => {
if (res.confirm) {
this.dialogVisible = true
}
}
})
}
if (!this.userInfo.fadadaConfirm) {
uni.showModal({
title: '提示',
content: '您尚未认证, 请前往认证',
showCancel: false,
success: res => {
if (res.confirm) {
uni.setStorageSync('user_tab', 0)
uni.navigateTo({url: '/pages/user/user'})
}
}
})
}
this.invoiceForm.phone = this.userInfo.mobile
this.invoiceForm.email = this.userInfo.businessCard ? this.userInfo.businessCard.email : ''
if (this.userInfo.accountType === 2) {
if (this.userInfo.fadadaConfirm && this.userInfo.company) {
this.invoiceForm.name = this.userInfo.company.companyName
this.invoiceForm.code = this.userInfo.company.organization
this.invoiceForm.address = this.userInfo.company.addressDetail
this.invoiceForm.tel = this.userInfo.company.mobile
this.invoiceForm.bankName = this.userInfo.company.companyBankName
this.invoiceForm.bankAccount = this.userInfo.company.companyAccount
}
} else {
this.invoiceForm.name = this.userInfo.person.personName
this.invoiceForm.code = this.userInfo.person.idCard
this.invoiceForm.address = this.userInfo.person.address
this.invoiceForm.tel = this.userInfo.mobile
}
}
})
},
closePdf() {
this.pdfUrl = ''
this.showPdf = false
},
openPayVoucher(row) {
// window.open(row.payVoucher)
uni.previewImage({
uni.previewImage( {
urls: [row.payVoucher],
})
},
openAiChat() {
uni.navigateTo({ url: '/pages/ai/chat' })
},
goTo(url) {
uni.navigateTo({ url })
},
async getPointsBalance() {
const res = await vk.callFunction({ url: 'client/points/kh/getBalance' })
if (res.code === 0 && res.data) {
this.pointsBalance = res.data.balance || 0
}
},
},
onShow() {
this.getQybPackage()
this.getSignIntro()
this.getSignFlow()
this.getSignTips()
vk.userCenter.getCurrentUserInfo({
success: async (data) => {
this.userInfo = data.userInfo
if (!this.userInfo.fadadaConfirm) {
uni.showModal({
title: '提示',
content: '您尚未认证, 请前往认证',
showCancel: false,
success: res => {
if (res.confirm) {
uni.setStorageSync('user_tab', 0)
uni.switchTab({url: '/pages/user/user'})
}
}
})
}
this.invoiceForm.phone = this.userInfo.mobile
this.invoiceForm.email = this.userInfo.businessCard ? this.userInfo.businessCard.email : ''
if (this.userInfo.accountType === 2) {
if (this.userInfo.fadadaConfirm && this.userInfo.company) {
this.invoiceForm.name = this.userInfo.company.companyName
this.invoiceForm.code = this.userInfo.company.organization
this.invoiceForm.address = this.userInfo.company.addressDetail
this.invoiceForm.tel = this.userInfo.company.mobile
this.invoiceForm.bankName = this.userInfo.company.companyBankName
this.invoiceForm.bankAccount = this.userInfo.company.companyAccount
}
} else {
this.invoiceForm.name = this.userInfo.person.personName
this.invoiceForm.code = this.userInfo.person.idCard
this.invoiceForm.address = this.userInfo.person.address
this.invoiceForm.tel = this.userInfo.mobile
}
}
})
this.getUserInfo()
this.getPointsBalance()
if (uni.getStorageSync('signInfo')) {
this.formData = JSON.parse(uni.getStorageSync('signInfo'))
if (this.formData.signEndDate) this.signEndDate = dayjs(this.formData.signEndDate).format('YYYY-MM-DD')
@@ -831,4 +880,23 @@ page {
.qyb-log-action {
flex: 1;
}
.ai-float-btn {
position: fixed;
bottom: 195px;
right: 30rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: linear-gradient(135deg, #a92924, #d4574e);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 20rpx rgba(169, 41, 36, 0.4);
z-index: 999;
}
.ai-float-icon {
font-size: 48rpx;
}
</style>