Files
jjx-wap-sign-only/pages/ai/review.vue
T
ngele a6cdf8642e feat: AI审查报告支持下载Word + 我的报告支持重新审查
- 云函数jjxReviewTimer完成审查后自动将HTML报告转换为Word文档并上传
- 新增buildDocx.js纯正则解析HTML报告生成A4公文版式docx(三级数据提取兼容结构漂移)
- 兼容智能体插件直接返回docx链接(jjx-docx-link标记协议)
- router/jjxDoReview同步支持Word生成(try降级不影响主流程)
- 前端报告列表新增下载Word/重新审查按钮
- 修复上传/查询取真实uid(vk.getVuex而非页面本地data)
2026-10-23 19:07:19 +08:00

360 lines
9.1 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="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: {
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: vk.getVuex('$user.userInfo._id') || '',
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>