Files
ngele c4ca8a87fa feat: 同步签约宝手机端功能到签约宝页面
- sign页面与签约宝精简版对齐:查看合同入口、AI积分区块(合同审查/我的报告/积分充值)
- 新增pages/ai/{points,review,reports}三个页面(与签约宝精简版一致)
- pages.json注册AI页面路由
2026-10-23 19:57:27 +08:00

369 lines
9.4 KiB
Vue
Raw Permalink 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="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>