c4ca8a87fa
- sign页面与签约宝精简版对齐:查看合同入口、AI积分区块(合同审查/我的报告/积分充值)
- 新增pages/ai/{points,review,reports}三个页面(与签约宝精简版一致)
- pages.json注册AI页面路由
369 lines
9.4 KiB
Vue
369 lines
9.4 KiB
Vue
<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> |