Files
jjx-wap-sign-only/pages/ai/reports.vue
T
ngele 4991d23ca8 fix: 我的报告页未登录引导登录,uid获取增加storage兜底
- 修复进入我的报告页即提示加载失败:未登录时云函数返回未登录被reject
- 新增getUid:vuex优先、uni_id_user_info storage兜底
- 未登录时不再发请求,弹窗引导去登录
2026-10-23 19:22:50 +08:00

359 lines
9.7 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>