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

361 lines
9.8 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="我的报告"></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>