feat: 新增AI客服/积分管理/合同审查/我的报告页面,完善.sign.vue入口与积分显示
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<view class="chat-wrap">
|
||||
<!-- 顶部导航 -->
|
||||
<uititle title="AI 智能客服"></uititle>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view scroll-y class="msg-list" :scroll-into-view="scrollId" scroll-with-animation
|
||||
:style="{ height: msgListHeight + 'px' }">
|
||||
|
||||
<!-- 欢迎消息 -->
|
||||
<view v-if="messages.length === 0" class="msg-item left" id="msg-welcome">
|
||||
<view class="avatar">🤖</view>
|
||||
<view class="bubble">
|
||||
您好!我是金久信签约宝AI智能客服,可以帮您解答金久信签约宝使用过程中注册、操作、适用场景等问题。请问有什么可以帮您?
|
||||
<view class="quick-btns">
|
||||
<view class="quick-btn" @click="quickSend('注册准备资料事项')">注册准备资料事项</view>
|
||||
<view class="quick-btn" @click="quickSend('关键字无法识别')">关键字无法识别</view>
|
||||
<view class="quick-btn" @click="quickSend('金久信签约宝法律效力')">金久信签约宝法律效力</view>
|
||||
<view class="quick-btn" @click="quickSend('电子签约不适用场景')">电子签约不适用场景</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 对话消息 -->
|
||||
<view v-for="(msg, i) in messages" :key="i" :id="'msg' + i"
|
||||
:class="['msg-item', msg.role === 'user' ? 'right' : 'left']">
|
||||
<view v-if="msg.role !== 'user'" class="avatar">🤖</view>
|
||||
<view class="bubble" v-html="formatContent(msg.content)"></view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="msg-item left" id="msg-loading">
|
||||
<view class="avatar">🤖</view>
|
||||
<view class="bubble loading">思考中...</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入栏 -->
|
||||
<view class="input-bar">
|
||||
<input v-model="inputText" @confirm="send" placeholder="输入您的问题..."
|
||||
:adjust-position="true" confirm-type="send" />
|
||||
<view class="send-btn" @click="send">发送</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
inputText: '',
|
||||
messages: [],
|
||||
loading: false,
|
||||
conversationId: null,
|
||||
scrollId: '',
|
||||
msgListHeight: 500
|
||||
};
|
||||
},
|
||||
onReady() {
|
||||
// 计算消息列表高度(屏幕高度 - 导航栏 - 输入栏)
|
||||
const sys = uni.getSystemInfoSync();
|
||||
this.msgListHeight = sys.windowHeight - 88 - 56;
|
||||
},
|
||||
methods: {
|
||||
formatContent(text) {
|
||||
if (!text) return '';
|
||||
let html = text.replace(/\*\*(.*?)\*\*/g, '<strong>$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;
|
||||
},
|
||||
quickSend(text) {
|
||||
this.inputText = text;
|
||||
this.send();
|
||||
},
|
||||
async send() {
|
||||
const msg = this.inputText.trim();
|
||||
if (!msg || this.loading) return;
|
||||
this.inputText = '';
|
||||
this.messages.push({ role: 'user', content: msg });
|
||||
this.loading = true;
|
||||
this.scrollToBottom();
|
||||
|
||||
try {
|
||||
const res = await this.vk.callFunction({
|
||||
url: 'client/ai/pub/chat',
|
||||
data: { message: msg, bot_id: '7683509749475819554' }
|
||||
});
|
||||
|
||||
if (res.data.conversation_id) this.conversationId = res.data.conversation_id;
|
||||
this.messages.push({ role: 'assistant', content: res.data.answer });
|
||||
} catch (e) {
|
||||
this.messages.push({ role: 'assistant', content: '网络异常,请稍后重试' });
|
||||
}
|
||||
this.loading = false;
|
||||
this.scrollToBottom();
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (this.messages.length === 0) {
|
||||
this.scrollId = 'msg-welcome';
|
||||
} else if (this.loading) {
|
||||
this.scrollId = 'msg-loading';
|
||||
} else {
|
||||
this.scrollId = 'msg' + (this.messages.length - 1);
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-wrap {
|
||||
height: 93vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
uni-scroll-view{
|
||||
width: auto !important;
|
||||
}
|
||||
.msg-list {
|
||||
flex: 1;
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
padding-top :24rpx;
|
||||
}
|
||||
|
||||
.msg-item {
|
||||
display: flex;
|
||||
margin-bottom: 24rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.msg-item.right {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #a92924, #d4574e);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 70%;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.msg-item.left .bubble {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
margin-left: 16rpx;
|
||||
border-top-left-radius: 8rpx;
|
||||
}
|
||||
|
||||
.msg-item.right .bubble {
|
||||
background: #a92924;
|
||||
color: #fff;
|
||||
margin-right: 16rpx;
|
||||
border-top-right-radius: 8rpx;
|
||||
}
|
||||
|
||||
.bubble.loading {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.quick-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 28rpx;
|
||||
font-size: 24rpx;
|
||||
border: 1px solid #a92924;
|
||||
color: #a92924;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.quick-btn:active {
|
||||
background: #a92924;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.input-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.input-bar input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 120rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
background: #a92924;
|
||||
color: #fff;
|
||||
border-radius: 36rpx;
|
||||
font-size: 28rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.send-btn:active {
|
||||
background: #982520;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<view class="page-wrap">
|
||||
<uititle title="积分管理"></uititle>
|
||||
|
||||
<!-- 积分概览 -->
|
||||
<view class="points-header">
|
||||
<view class="ph-card main">
|
||||
<view class="ph-label">可用积分</view>
|
||||
<view class="ph-value">{{ pointsBalance }}</view>
|
||||
</view>
|
||||
<view class="ph-card sub">
|
||||
<view class="ph-label">累计获得</view>
|
||||
<view class="ph-value">{{ pointsTotalEarned }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 充值套餐 -->
|
||||
<view class="section-title">💎 积分充值</view>
|
||||
<view class="recharge-grid">
|
||||
<view v-for="(pkg, i) in rechargePackages" :key="i"
|
||||
:class="['recharge-card', selectedRecharge === i && 'selected']"
|
||||
@click="selectedRecharge = i">
|
||||
<view class="rc-points">{{ pkg.points }}</view>
|
||||
<view class="rc-price">{{ pkg.price }}</view>
|
||||
<view v-if="pkg.bonus" class="rc-bonus">{{ pkg.bonus }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="p-20">
|
||||
<u-button type="primary" shape="circle" @click="doRecharge">
|
||||
立即充值 {{ rechargePackages[selectedRecharge].price }}({{ rechargePackages[selectedRecharge].points }} 积分)
|
||||
</u-button>
|
||||
</view>
|
||||
|
||||
<!-- 积分明细 -->
|
||||
<view class="section-title">📊 积分明细</view>
|
||||
<view v-if="pointsLogList.length === 0" class="empty-tip">暂无积分记录</view>
|
||||
<view v-for="(item, i) in pointsLogList" :key="i" class="log-item">
|
||||
<view class="log-left">
|
||||
<view class="log-reason">{{ item.reason }}</view>
|
||||
<view class="log-time">{{ item._add_time_str }}</view>
|
||||
</view>
|
||||
<view :class="['log-amount', item.type === 'earn' ? 'green' : 'red']">
|
||||
{{ item.type === 'earn' ? '+' : '-' }}{{ Math.abs(item.amount) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import uititle from '@/components/common/title'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export default {
|
||||
components: { uititle },
|
||||
data() {
|
||||
return {
|
||||
pointsBalance: 0,
|
||||
pointsTotalEarned: 0,
|
||||
pointsLogList: [],
|
||||
selectedRecharge: 1,
|
||||
rechargePackages: [
|
||||
{ points: 10000, price: '20元', bonus: '' },
|
||||
{ points: 50000, price: '100元', bonus: '' },
|
||||
{ points: 100000, price: '200元', bonus: '' },
|
||||
{ points: 450000, price: '900元', bonus: '' },
|
||||
{ points: 1000000, price: '2000元', bonus: '' },
|
||||
{ points: 2000000, price: '4000元', bonus: '' },
|
||||
]
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.getPointsBalance()
|
||||
this.getPointsLog()
|
||||
},
|
||||
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
|
||||
this.pointsTotalEarned = res.data.total_earned || 0
|
||||
}
|
||||
},
|
||||
async getPointsLog() {
|
||||
const res = await vk.callFunction({
|
||||
url: 'client/points/kh/getLog',
|
||||
data: { pageIndex: 1, pageSize: 50 }
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
this.pointsLogList = (res.data.rows || []).map(r => ({
|
||||
...r,
|
||||
_add_time_str: dayjs(r._add_time && r._add_time['$date'] ? r._add_time['$date'] : r._add_time).format('YYYY-MM-DD HH:mm')
|
||||
}))
|
||||
}
|
||||
},
|
||||
async doRecharge() {
|
||||
const pkg = this.rechargePackages[this.selectedRecharge]
|
||||
const priceMap = {
|
||||
'20元': 2000, '100元': 10000, '200元': 20000,
|
||||
'900元': 90000, '2000元': 200000, '4000元': 400000
|
||||
}
|
||||
const price = priceMap[pkg.price] || 2000
|
||||
const res = await vk.callFunction({
|
||||
url: 'client/points/kh/createOrder',
|
||||
data: { points: pkg.points, price }
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
uni.navigateTo({ url: '/pages/shop/pay?id=' + res.data })
|
||||
} else {
|
||||
uni.showToast({ title: res.msg || '创建订单失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-wrap {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.points-header {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.ph-card {
|
||||
flex: 1;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ph-card.main {
|
||||
background: linear-gradient(135deg, #a92924, #d4574e);
|
||||
}
|
||||
|
||||
.ph-card.sub {
|
||||
background: linear-gradient(135deg, #333, #666);
|
||||
}
|
||||
|
||||
.ph-label {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.ph-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
padding: 20rpx 30rpx 10rpx;
|
||||
}
|
||||
|
||||
.recharge-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.recharge-card {
|
||||
flex: 0 0 calc(33.33% - 12rpx);
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 12rpx;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.recharge-card.selected {
|
||||
border-color: #a92924;
|
||||
background: #fdf0f0;
|
||||
}
|
||||
|
||||
.rc-points {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.rc-price {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.rc-bonus {
|
||||
position: absolute;
|
||||
top: -2rpx;
|
||||
right: -2rpx;
|
||||
background: #a92924;
|
||||
color: #fff;
|
||||
font-size: 18rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 0 10rpx 0 10rpx;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 30rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.log-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.log-reason {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.log-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-amount.green { color: #67c23a; }
|
||||
.log-amount.red { color: #f56c6c; }
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,264 @@
|
||||
<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.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>
|
||||
</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: {
|
||||
async queryReports() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await vk.callFunction({
|
||||
url: 'client/ai/pub/getMyReports',
|
||||
data: { pageIndex: 1, pageSize: 50 }
|
||||
})
|
||||
if (res.code === 0 && res.data && res.data.rows) {
|
||||
this.reportList = res.data.rows.map(r => ({
|
||||
fileName: r.file_name,
|
||||
fileSize: r.file_size,
|
||||
fileUrl: r.file_url || '',
|
||||
htmlUrl: r.html_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
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,359 @@
|
||||
<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,
|
||||
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>
|
||||
+56
-2
@@ -15,15 +15,32 @@
|
||||
<text class="text-main">您剩余金久信签约宝次数: {{ userInfo.qybNum }}次</text>
|
||||
<view class="qyb-action-row flex justify-start items-center mt-20">
|
||||
<view class="qyb-buy-action">
|
||||
<u-button size="mini" type="primary" @click.native="dialogVisible = true"
|
||||
<u-button size="max" type="primary" @click.native="dialogVisible = true"
|
||||
:custom-style="{padding: 0, width: '100%'}">立即购买</u-button>
|
||||
</view>
|
||||
<view class="qyb-log-action ml-25">
|
||||
<u-button size="mini" type="success" @click.native="openLog"
|
||||
<u-button size="max" type="success" @click.native="openLog"
|
||||
:custom-style="{padding: 0, width: '100%'}">明细</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pb-30">
|
||||
<text class="text-main">您AI积分剩余: {{ pointsBalance }}积分</text>
|
||||
<view class="qyb-action-row flex justify-start items-center mt-20" style="display: flex;justify-content:space-between;">
|
||||
<view class="qyb-buy-action">
|
||||
<u-button size="max" type="warning" @click="vk.navigateTo('/pages/ai/review')"
|
||||
:custom-style="{padding: 0, width: '100%'}">合同审查</u-button>
|
||||
</view>
|
||||
<view class="qyb-buy-action">
|
||||
<u-button size="max" type="success" @click="vk.navigateTo('/pages/ai/reports')"
|
||||
:custom-style="{padding: 0, width: '100%'}">我的报告</u-button>
|
||||
</view>
|
||||
<view class="qyb-buy-action">
|
||||
<u-button size="max" type="primary" @click="vk.navigateTo('/pages/ai/points')"
|
||||
:custom-style="{padding: 0, width: '100%'}">积分充值</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-main mt-10">温馨提示:尊敬的用户如果您的剩余金久信签约宝次数为0次,请先进行购买,再进行签约。</view>
|
||||
<view class="card m-20">
|
||||
<view class="flex justify-between items-center" @click="showSignIntro = !showSignIntro">
|
||||
@@ -347,6 +364,10 @@
|
||||
</view>
|
||||
</el-dialog>
|
||||
<collapse></collapse>
|
||||
<!-- AI 客服悬浮按钮 -->
|
||||
<view class="ai-float-btn" @click="openAiChat">
|
||||
<text class="ai-float-icon">🤖</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -381,6 +402,7 @@ export default {
|
||||
userInfo: {
|
||||
qybNum: 0
|
||||
},
|
||||
pointsBalance: 0,
|
||||
pdfUrl: '',
|
||||
show: false,
|
||||
showPdf: false,
|
||||
@@ -801,6 +823,18 @@ export default {
|
||||
urls: [row.payVoucher],
|
||||
})
|
||||
},
|
||||
openAiChat() {
|
||||
uni.navigateTo({ url: '/pages/ai/chat' })
|
||||
},
|
||||
goTo(url) {
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
async getPointsBalance() {
|
||||
const res = await vk.callFunction({ url: 'client/points/kh/getBalance' })
|
||||
if (res.code === 0 && res.data) {
|
||||
this.pointsBalance = res.data.balance || 0
|
||||
}
|
||||
},
|
||||
},
|
||||
onShow() {
|
||||
this.getQybPackage()
|
||||
@@ -808,6 +842,7 @@ export default {
|
||||
this.getSignFlow()
|
||||
this.getSignTips()
|
||||
this.getUserInfo()
|
||||
this.getPointsBalance()
|
||||
if (uni.getStorageSync('signInfo')) {
|
||||
this.formData = JSON.parse(uni.getStorageSync('signInfo'))
|
||||
if (this.formData.signEndDate) this.signEndDate = dayjs(this.formData.signEndDate).format('YYYY-MM-DD')
|
||||
@@ -845,4 +880,23 @@ page {
|
||||
.qyb-log-action {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ai-float-btn {
|
||||
position: fixed;
|
||||
bottom: 195px;
|
||||
right: 30rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #a92924, #d4574e);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(169, 41, 36, 0.4);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.ai-float-icon {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user