feat: 添加AI智能客服(弹窗+嵌入式聊天页面,对接扣子API)

This commit is contained in:
2026-10-10 18:34:03 +08:00
parent 6f1b2dc03a
commit edd53d5d2b
4 changed files with 2317 additions and 1835 deletions
+217
View File
@@ -0,0 +1,217 @@
<template>
<view v-if="visible" class="ai-chat-panel">
<!-- 头部 -->
<view class="chat-header">
<text>🤖 金久信 AI 智能客服</text>
<text class="close" @click="visible = false">×</text>
</view>
<!-- 消息列表 -->
<scroll-view scroll-y class="msg-list" :scroll-into-view="scrollId" scroll-with-animation>
<!-- 欢迎消息(仅在没有对话时显示) -->
<view v-if="messages.length === 0" class="msg-item assistant" id="msg-welcome">
<view class="bubble">
您好!我是金久信签约宝AI智能客服,可以帮您解答金久信签约宝使用过程中注册、操作、适用场景等问题。请问有什么可以帮您?
<view class="quick-btns">
<view class="quick-btn" @click="chatQuickSend('注册准备资料事项')">注册准备资料事项</view>
<view class="quick-btn" @click="chatQuickSend('关键字无法识别')">关键字无法识别</view>
<view class="quick-btn" @click="chatQuickSend('如何下询价单')">金久信签约宝法律效力</view>
<view class="quick-btn" @click="chatQuickSend('电子签约不适用场景')">电子签约不适用场景</view>
</view>
</view>
</view>
<!-- 对话消息 -->
<view v-for="(msg, i) in messages" :key="i" :id="'msg' + i" :class="['msg-item', msg.role]">
<view class="bubble" v-html="msg.content"></view>
</view>
<view v-if="loading" class="msg-item assistant" id="msg-loading">
<view class="bubble typing">思考中...</view>
</view>
</scroll-view>
<!-- 输入框 -->
<view class="input-bar">
<input v-model="inputText" @confirm="send" placeholder="输入您的问题..." />
<button @click="send">发送</button>
</view>
</view>
</template>
<script>
export default {
name: "ai-chat",
data() {
return {
visible: false,
inputText: '',
messages: [],
loading: false,
conversationId: null,
scrollId: ''
};
},
methods: {
toggle() {
this.visible = !this.visible;
if (this.visible) {
this.$nextTick(() => this.scrollToBottom());
}
},
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">
.ai-chat-panel {
position: fixed;
bottom: 90px;
right: 50px;
width: 380px;
height: 520px;
background: #fff;
border-radius: 16px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.2);
z-index: 99999;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
padding: 12px 16px;
background: linear-gradient(135deg, #a92924, #d4574e);
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
font-weight: 600;
}
.chat-header .close {
cursor: pointer;
font-size: 22px;
opacity: 0.8;
}
.chat-header .close:hover { opacity: 1; }
.msg-list {
flex: 1;
padding: 12px;
background: #f7f8fa;
overflow-y: auto;
}
.msg-item { margin-bottom: 10px; display: flex; }
.msg-item.user { justify-content: flex-end; }
.msg-item.assistant { justify-content: flex-start; }
.msg-item .bubble {
max-width: 75%;
padding: 10px 14px;
border-radius: 12px;
font-size: 13px;
line-height: 1.6;
word-break: break-word;
}
.msg-item.user .bubble {
background: #a92924;
color: #fff;
border-top-right-radius: 4px;
}
.msg-item.assistant .bubble {
background: #fff;
color: #333;
border: 1px solid #eee;
border-top-left-radius: 4px;
}
.msg-item.assistant .bubble.typing { color: #999; }
.quick-btns {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.quick-btn {
padding: 5px 12px;
border-radius: 14px;
font-size: 12px;
cursor: pointer;
border: 1px solid #a92924;
color: #a92924;
background: #fff;
transition: all 0.2s;
}
.quick-btn:hover {
background: #a92924;
color: #fff;
}
.input-bar {
padding: 10px 12px;
border-top: 1px solid #eee;
display: flex;
gap: 8px;
background: #fff;
}
.input-bar input {
flex: 1;
height: 36px;
border: 1px solid #ddd;
border-radius: 8px;
padding: 0 10px;
font-size: 13px;
}
.input-bar input:focus { border-color: #a92924; outline: none; }
.input-bar button {
padding: 0 16px;
height: 36px;
background: #a92924;
color: #fff;
border: none;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
}
.input-bar button:hover { background: #982520; }
</style>
+3 -4
View File
@@ -22,13 +22,11 @@
<!-- </a>-->
<!-- </li>-->
<li class="side-steel-kefu">
<a rel="nofollow"
href="http://wpa.qq.com/msgrd?v=3&amp;uin=949419497&amp;site=qq&amp;menu=yes"
target="_blank">
<a rel="nofollow" href="javascript:;" @click="$refs.aiChat.toggle()">
<div class="item">
<i class="wag-icon"><img src="@/static/images/cl4.png"></i>
</div>
<span>在线客服</span>
<span>AI 客服</span>
</a>
</li>
<!-- <li class="side-steel-counter">-->
@@ -66,6 +64,7 @@
</div>
</div>
</div>
<ai-chat ref="aiChat" />
</view>
</template>
+375 -109
View File
@@ -60,7 +60,23 @@
</view>
<text class="ml-20">认证信息变更</text>
</view>
<div class="menu-group-title">AI 智能服务</div>
<el-menu @select="menuSelect" :default-active="menuIndex" class="el-menu-vertical-demo">
<el-menu-item index="3">
<span class="icon">🤖</span> AI 客服
</el-menu-item>
<el-menu-item index="4">
<span class="icon">📋</span> 合同审查
</el-menu-item>
<el-menu-item index="5">
<span class="icon">📈</span> 我的报告
</el-menu-item>
<el-menu-item index="6">
<span class="icon">💎</span> 积分管理
</el-menu-item>
</el-menu>
</el-col>
<el-col :span="18">
<view class="flex flex-col justify-start items-start" v-if="isSignLog">
<view class="trade-method">
@@ -146,7 +162,8 @@
</view>
</template>
</view>
<view class="contract-list" v-if="!isSign && !isSignIntro && !isSignLog && !isSignFlow && !isSignTips">
<view class="contract-list"
v-if="!isSign && !isSignIntro && !isSignLog && !isSignFlow && !isSignTips">
<view class="trade-method">
<!-- <view @click="changeTradeMethod(null)" class="trade-method-item"-->
<!-- :class="{active: (formData.tradeMethod === null && formData.type === null)}">全部-->
@@ -195,30 +212,24 @@
</el-tag>
<template v-if="item.sheet">
<view class="ml-20">
<el-tag size="mini" v-if="item.sheet.tradeMethod === 0" type="primary"
>自主交易
<el-tag size="mini" v-if="item.sheet.tradeMethod === 0" type="primary">自主交易
</el-tag>
<el-tag size="mini" v-if="item.sheet.tradeMethod === 1" type="success"
>金久信公证
<el-tag size="mini" v-if="item.sheet.tradeMethod === 1" type="success">金久信公证
</el-tag>
</view>
<view class="ml-20">
<el-tag size="mini" effect="plain" v-if="item.sheet.signMethod === 0"
type="success"
>标准合同
type="success">标准合同
</el-tag>
<el-tag size="mini" effect="plain" v-if="item.sheet.signMethod === 2"
type="primary"
>双方协商合同
type="primary">双方协商合同
</el-tag>
<el-tag size="mini" effect="plain" v-if="item.sheet.signMethod === 1"
type="warning"
>线下合同
type="warning">线下合同
</el-tag>
</view>
</template>
<el-tag size="mini" v-if="item.contract.tradeMethod === 1" type="success"
>金久信公证
<el-tag size="mini" v-if="item.contract.tradeMethod === 1" type="success">金久信公证
</el-tag>
<view class="contract-product" v-if="item.contract.status > 0">
{{ item.contract.docTitle }}
@@ -271,14 +282,16 @@
dayjs(item.contract.signEndDate).format('YYYY-MM-DD HH:mm:ss')
}}
</view>
<view v-if="item.contract.type === 'sign' && item.contract.payUser && item.contract.payUser.user">
<view
v-if="item.contract.type === 'sign' && item.contract.payUser && item.contract.payUser.user">
费用承担方:{{
item.contract.payUser.user.accountType === 2 ?
item.contract.payUser.user.company.companyName : item.contract.payUser.user.person.personName
}}
</view>
<template v-if="parseInt(item.contract.status) > 0">
<view v-for="(party, partyIndex) in item.contract.partyList.filter(p => p.role !== 'third')"
<view
v-for="(party, partyIndex) in item.contract.partyList.filter(p => p.role !== 'third')"
:key="`p_${partyIndex}`">
<template v-if="party.userId === item.contract.userId">
发起方签订完成时间:{{
@@ -296,7 +309,8 @@
}}
</template>
</view>
<view v-for="(party, partyIndex) in item.contract.partyList.filter(p => p.role === 'third')"
<view
v-for="(party, partyIndex) in item.contract.partyList.filter(p => p.role === 'third')"
:key="`t_${partyIndex}`">
第{{ numToChForThird[partyIndex] }}方签订完成时间:{{
party.signedAt ? dayjs(party.signedAt).format('YYYY-MM-DD HH:mm:ss') : ''
@@ -312,9 +326,8 @@
v-if="item.contract.status === 0 && item.contract.userId === vk.getVuex('$user.userInfo._id')"
@click="createContract(item.contract)" size="mini" type="primary">创建合同
</el-button>
<el-button
v-if="item.contract.isMine"
@click="delContract(item.contract)" size="mini" type="text">
<el-button v-if="item.contract.isMine" @click="delContract(item.contract)"
size="mini" type="text">
删除合同
</el-button>
<template v-if="parseInt(item.contract.status) === 1">
@@ -324,18 +337,14 @@
</el-button>
</template>
<template v-if="parseInt(item.contract.status) === 2">
<el-button
@click="viewContract(item.contract)"
size="mini" plain>
<el-button @click="viewContract(item.contract)" size="mini" plain>
{{ parseInt(item.contract.signType) === 1 ? '查看单据' : '查看合同' }}
</el-button>
<el-button @click="downloadContract(item.contract.transactionId)"
type="primary"
<el-button @click="downloadContract(item.contract.transactionId)" type="primary"
size="mini" plain>
{{ parseInt(item.contract.signType) === 1 ? '下载单据' : '下载合同' }}
</el-button>
<el-button @click="payDownloadContract(item.contract)"
size="mini" plain>
<el-button @click="payDownloadContract(item.contract)" size="mini" plain>
{{
parseInt(item.contract.signType) === 1 ? '下载单据(付费司法调证)' : '下载合同(付费司法调证)'
}}
@@ -350,10 +359,8 @@
(item.order && item.order.orderStatus === 4) ? '订单已结束' : ([5, 6].includes(item.order.orderStatus) ? '订单已终止' : (item.role === 'buyer' ? '去付款' : '去请款'))
}}
</el-button>
<el-button
v-if="item.contract.otherFile && item.contract.otherFile.length > 0"
@click="openDownload(item.contract.otherFile)" plain
size="mini"
<el-button v-if="item.contract.otherFile && item.contract.otherFile.length > 0"
@click="openDownload(item.contract.otherFile)" plain size="mini"
type="info">查看附件
</el-button>
</view>
@@ -365,8 +372,8 @@
<view class="">
<el-button @click.native="changeContract(item.contract)"
v-if="((item.sheet && item.sheet.signMethod === 2) || item.contract.type === 'sign' || item.contract.type === 'self-support') && item.contract.status === 1
&& item.contract.userId === vk.getVuex('$user.userInfo._id') && item.contract.signCount === 0"
size="mini" type="primary">修改合同内容
&& item.contract.userId === vk.getVuex('$user.userInfo._id') && item.contract.signCount === 0" size="mini"
type="primary">修改合同内容
</el-button>
</view>
</view>
@@ -384,21 +391,20 @@
</view>
<view class="text-main mt-10">温馨提示:尊敬的用户如果您的剩余金久信签约宝次数为0次,请先进行购买,再进行签约。</view>
<vk-data-form ref="form2" v-model="form2.data" :action="form2.props.action"
:columns="form2.props.columns"
:rules="form2.props.rules" :form-type="form2.props.formType"
:loading.sync="form2.props.loading" :show-cancel="false"
:auto-close="true" label-width="140px" @success="onFormSuccess">
:columns="form2.props.columns" :rules="form2.props.rules" :form-type="form2.props.formType"
:loading.sync="form2.props.loading" :show-cancel="false" :auto-close="true"
label-width="140px" @success="onFormSuccess">
<template v-slot:download>
<el-button size="mini" type="text" @click="downloadContractTemplate">点击下载
</el-button>
</template>
<template v-slot:contractFile>
<vk-data-upload v-model="form2.data.contractFile" :cloudPathRemoveChinese="false" list-type="text"
accept=".pdf" :on-success="uploadSuccess" ref="uploader" :before-upload="beforeUpload"
:show-file-list="false">
<vk-data-upload v-model="form2.data.contractFile" :cloudPathRemoveChinese="false"
list-type="text" accept=".pdf" :on-success="uploadSuccess" ref="uploader"
:before-upload="beforeUpload" :show-file-list="false">
</vk-data-upload>
<view class="text-main text-24 my-15 pointer" @click="openPreview(form2.data.contractFile)"
v-if="form2.data.contractFile">预览:
<view class="text-main text-24 my-15 pointer"
@click="openPreview(form2.data.contractFile)" v-if="form2.data.contractFile">预览:
{{ form2.data.contractFile[form2.data.contractFile.length - 1].split('-').pop() }}
</view>
<view class="tips text-24 text-gray">
@@ -406,11 +412,8 @@
</view>
</template>
<template v-slot:signEndDate>
<el-date-picker
:popper-class="'currentDatePickerClass'"
v-model="form2.data.signEndDate"
type="datetime"
placeholder="选择签署截止日期">
<el-date-picker :popper-class="'currentDatePickerClass'"
v-model="form2.data.signEndDate" type="datetime" placeholder="选择签署截止日期">
</el-date-picker>
</template>
<template v-slot:tips>
@@ -424,8 +427,7 @@
class="flex justify-start items-center mb-10 ">
<span>第{{ numToChForThird[index] }}方</span>
<div class="mx-15">
<el-input v-model="form2.data.thirdList[index].phone"
placeholder="请输入第三方手机号"/>
<el-input v-model="form2.data.thirdList[index].phone" placeholder="请输入第三方手机号" />
</div>
<div>
<el-input v-model="form2.data.thirdList[index].signKeyword"
@@ -434,8 +436,7 @@
<div class="flex justify-start items-center">
<i class="el-icon-circle-plus pointer text-main text-50 ml-35"
@click="addThird" />
<i class="el-icon-remove pointer text-50 ml-35"
@click="removeThird(index)"/>
<i class="el-icon-remove pointer text-50 ml-35" @click="removeThird(index)" />
<i title="上移" class="el-icon-arrow-up pointer text-50 ml-35" v-if="index > 0"
@click="thirdMoveUp(index)" />
<i title="下移" class="el-icon-arrow-down pointer text-50 ml-35"
@@ -448,7 +449,8 @@
</template>
<template v-slot:footer="{ loading }">
<view style="text-align: center;">
<el-button :loading="loading" type="primary" @click="form2.props.action">提交</el-button>
<el-button :loading="loading" type="primary"
@click="form2.props.action">提交</el-button>
</view>
</template>
</vk-data-form>
@@ -462,6 +464,39 @@
<view v-if="isSignTips" class="bg-white p-20">
<div v-html="signTips"></div>
</view>
<!-- AI 客服(嵌入式) -->
<view v-if="menuIndex === '3'" class="ai-chat-embed">
<view class="chat-header-bar">
<text class="chat-title">🤖 金久信签约宝 AI 智能客服</text>
</view>
<scroll-view scroll-y class="chat-msg-list" :scroll-into-view="chatScrollId" scroll-with-animation>
<view v-if="chatMessages.length === 0" class="chat-row left" id="chat-welcome">
<view class="chat-avatar">🤖</view>
<view class="chat-bubble">
您好!我是金久信签约宝AI智能客服,可以帮您解答金久信签约宝使用过程中注册、操作、适用场景等问题。请问有什么可以帮您?
<view class="quick-btns">
<view class="quick-btn" @click="chatQuickSend('注册准备资料事项')">注册准备资料事项</view>
<view class="quick-btn" @click="chatQuickSend('关键字无法识别')">关键字无法识别</view>
<view class="quick-btn" @click="chatQuickSend('如何下询价单')">金久信签约宝法律效力</view>
<view class="quick-btn" @click="chatQuickSend('电子签约不适用场景')">电子签约不适用场景</view>
</view>
</view>
</view>
<view v-for="(msg, i) in chatMessages" :key="i" :id="'chat-msg' + i"
:class="['chat-row', msg.role === 'user' ? 'right' : 'left']">
<view v-if="msg.role !== 'user'" class="chat-avatar">🤖</view>
<view class="chat-bubble" v-html="msg.content"></view>
</view>
<view v-if="chatLoading" class="chat-row left" id="chat-loading">
<view class="chat-avatar">🤖</view>
<view class="chat-bubble typing">思考中...</view>
</view>
</scroll-view>
<view class="chat-input-bar">
<input v-model="chatInput" @confirm="chatSend" placeholder="输入您的问题..." />
<view class="chat-send-btn" @click="chatSend">发送</view>
</view>
</view>
</el-col>
</el-row>
@@ -470,9 +505,8 @@
<el-dialog :visible.sync="form1.props.show" width="70%">
<vk-data-form ref="form1" v-model="form1.data" :action="form1.props.action" :columns="form1.props.columns"
:rules="form1.props.rules" :form-type="form1.props.formType"
:loading.sync="form1.props.loading" :show-cancel="false"
:auto-close="true" label-width="140px" @success="onFormSuccess">
:rules="form1.props.rules" :form-type="form1.props.formType" :loading.sync="form1.props.loading"
:show-cancel="false" :auto-close="true" label-width="140px" @success="onFormSuccess">
<template v-slot:contractFile>
<vk-data-upload v-model="form1.data.contractFile" :cloudPathRemoveChinese="false" list-type="text"
accept=".pdf" :on-success="uploadSuccess1" ref="uploader1" :before-upload="beforeUpload"
@@ -494,23 +528,18 @@
class="flex justify-start items-center mb-10 ">
<span>第{{ numToChForThird[index] }}方</span>
<div class="mx-15">
<el-input v-model="form1.data.thirdList[index].phone"
placeholder="请输入第三方手机号"/>
<el-input v-model="form1.data.thirdList[index].phone" placeholder="请输入第三方手机号" />
</div>
<div>
<el-input v-model="form1.data.thirdList[index].signKeyword"
placeholder="请输入第三方签章关键字"/>
<el-input v-model="form1.data.thirdList[index].signKeyword" placeholder="请输入第三方签章关键字" />
</div>
<div class="flex justify-start items-center">
<i class="el-icon-circle-plus pointer text-main text-50 ml-35"
@click="addThird1"/>
<i class="el-icon-remove pointer text-50 ml-35"
@click="removeThird1(index)"/>
<i class="el-icon-circle-plus pointer text-main text-50 ml-35" @click="addThird1" />
<i class="el-icon-remove pointer text-50 ml-35" @click="removeThird1(index)" />
<i title="上移" class="el-icon-arrow-up pointer text-50 ml-35" v-if="index > 0"
@click="thirdMoveUp1(index)" />
<i title="下移" class="el-icon-arrow-down pointer text-50 ml-35"
@click="thirdMoveDown1(index)"
v-if="index < form1.data.thirdList.length - 1"/>
@click="thirdMoveDown1(index)" v-if="index < form1.data.thirdList.length - 1" />
</div>
</div>
<p class="text-25 text-gray mt-20">如无第三方签约可忽略,签章关键字使用方法同下。</p>
@@ -529,8 +558,7 @@
<div class="flex justify-center items-center">
<div class="p-20 rounded m-20 shadow flex flex-col justify-center items-center flex-1 pointer"
:class="[selectedQyb === item._id ? 'buy-quote-mode-active' : '']"
v-for="(item, index) in qybPackageList" :key="index"
@click="selectedQyb = item._id">
v-for="(item, index) in qybPackageList" :key="index" @click="selectedQyb = item._id">
<p>{{ item.amount }}元</p>
<p>可获得{{ item.getNum }}次</p>
</div>
@@ -581,17 +609,39 @@
<el-dialog title="支付凭证" :visible.sync="showPayVoucher" width="50%">
<el-image :src="payVoucher" :preview-src-list="[payVoucher]" />
</el-dialog>
<!-- 右侧悬浮工具栏 -->
<div class="float-toolbar">
<div class="tool-item ai-hot">
<a rel="nofollow" href="javascript:;" @click="$refs.aiChat.toggle()">
<div class="item">
<i class="wag-icon"><img src="@/static/images/cl4.png"></i>
</div>
<span class="tool-label">AI客服</span>
</a>
</div>
<div class="tool-item ai-hot">
<span class="tool-icon">📑</span>
<span class="tool-label">合同审查</span>
</div>
</div>
<ai-chat ref="aiChat" />
</view>
</template>
<script>
import {Agreement} from "@icon-park/vue";
import {
Agreement
} from "@icon-park/vue";
import dayjs from "dayjs";
import {Loading} from "element-ui";
import {
Loading
} from "element-ui";
export default {
components: {Agreement},
components: {
Agreement
},
data() {
return {
numToChForThird: ['三', '四', '五', '六', '七', '八', '九', '十'],
@@ -627,6 +677,12 @@ export default {
showInvoice: false,
isSignTips: false,
signTips: '',
// AI 客服数据
chatMessages: [],
chatInput: '',
chatLoading: false,
chatConversationId: null,
chatScrollId: '',
invoiceForm: {
id: null,
name: null,
@@ -650,7 +706,10 @@ export default {
buyerSignKeyword: '',
sellerSignKeyword: '',
otherFile: '',
thirdList: [{phone: '', signKeyword: ''}],
thirdList: [{
phone: '',
signKeyword: ''
}],
},
// 表单属性
props: {
@@ -663,7 +722,9 @@ export default {
thirdKeywords.push(item.signKeyword)
}
}
uni.showLoading({title: '查验中'})
uni.showLoading({
title: '查验中'
})
let file = this.form1.data.contractFile[this.form1.data.contractFile.length - 1]
if (Array.isArray(file)) file = file[file.length - 1]
vk.request({
@@ -690,7 +751,8 @@ export default {
else {
uni.hideLoading()
this.form1.props.loading = true
this.form1.data.thirdList = this.form1.data.thirdList.filter(item => item.phone && item.signKeyword)
this.form1.data.thirdList = this.form1.data.thirdList.filter(
item => item.phone && item.signKeyword)
if (this.form1.data.thirdList.length > 0) {
for (let i = 0; i < this.form1.data.thirdList.length; i++) {
const item = this.form1.data.thirdList[i]
@@ -721,7 +783,9 @@ export default {
this.form1.props.loading = false
return false
}
const res = await this.$confirm(`1、您确认,若全体各签约方未完成全部签约流程,本签约文件不发生法律效力,系统无法对该签约文件予以保存及提供司法调证服务;<br>2、您确认,当前签约的实名主体名称、手机号码与注册认证阶段提交的信息未发生变更;若发生变更,必须及时进行重新认证后方可继续签约,避免产生严重法律风险与经济损失;<br>3、您确认,本次签约为本系统实名认证主体操作,如若不是实名认证主体操作,授权委托人需本系统进行授权认证后签约;<br>4、您确认,您与其他签约方均系在平等、自愿的基础上完成本次签约;<br>5、你确认,此次签约内容符合《中华人民共和国电子签名法》《民法典》生效保护范畴;<br>6、如因法律法规、监管部门政策更新等原因,系统提示您重新进行实名认证、证书续期等重要提示时,请您必须在签约前完成,否则造成任何损失,平台不承担任何责任;以上签约条件您均已经知悉并确认,否则您有权拒绝本次签约。`, '提示', {
const res = await this.$confirm(
`1、您确认,若全体各签约方未完成全部签约流程,本签约文件不发生法律效力,系统无法对该签约文件予以保存及提供司法调证服务;<br>2、您确认,当前签约的实名主体名称、手机号码与注册认证阶段提交的信息未发生变更;若发生变更,必须及时进行重新认证后方可继续签约,避免产生严重法律风险与经济损失;<br>3、您确认,本次签约为本系统实名认证主体操作,如若不是实名认证主体操作,授权委托人需本系统进行授权认证后签约;<br>4、您确认,您与其他签约方均系在平等、自愿的基础上完成本次签约;<br>5、你确认,此次签约内容符合《中华人民共和国电子签名法》《民法典》生效保护范畴;<br>6、如因法律法规、监管部门政策更新等原因,系统提示您重新进行实名认证、证书续期等重要提示时,请您必须在签约前完成,否则造成任何损失,平台不承担任何责任;以上签约条件您均已经知悉并确认,否则您有权拒绝本次签约。`,
'提示', {
confirmButtonText: '知悉同意',
showCancelButton: false,
type: 'warning',
@@ -732,7 +796,9 @@ export default {
url: 'client/contract/kh/launch',
data: this.form1.data
})
vk.redirectTo({url: '/pages/contract/index?tab=1'})
vk.redirectTo({
url: '/pages/contract/index?tab=1'
})
}
}
uni.hideLoading()
@@ -741,8 +807,7 @@ export default {
},
// 表单字段显示规则
columns: [
{
columns: [{
key: "title",
title: "标题",
type: "text"
@@ -850,7 +915,10 @@ export default {
tradeMethod: 0,
payBy: 0,
otherFile: [],
thirdList: [{phone: '', signKeyword: ''}],
thirdList: [{
phone: '',
signKeyword: ''
}],
},
// 表单属性
props: {
@@ -863,7 +931,9 @@ export default {
thirdKeywords.push(item.signKeyword)
}
}
uni.showLoading({title: '查验中'})
uni.showLoading({
title: '查验中'
})
vk.request({
method: "POST",
url: 'https://hcsc.gxwebsoft.com/api/read-pdf',
@@ -871,7 +941,8 @@ export default {
"content-type": "application/json; charset=utf-8",
},
data: {
file: this.form2.data.contractFile[this.form2.data.contractFile.length - 1],
file: this.form2.data.contractFile[this.form2.data.contractFile.length -
1],
buyerSignKeyword: this.form2.data.buyerSignKeyword,
sellerSignKeyword: this.form2.data.sellerSignKeyword,
thirdKeywords
@@ -888,7 +959,8 @@ export default {
else {
uni.hideLoading()
this.form2.props.loading = true
this.form2.data.thirdList = this.form2.data.thirdList.filter(item => item.phone && item.signKeyword)
this.form2.data.thirdList = this.form2.data.thirdList.filter(
item => item.phone && item.signKeyword)
if (this.form2.data.thirdList.length > 0) {
for (let i = 0; i < this.form2.data.thirdList.length; i++) {
const item = this.form2.data.thirdList[i]
@@ -914,7 +986,8 @@ export default {
this.form2.props.loading = false
return false
}
if (this.form2.data.payBy === undefined || this.form2.data.payBy === '' || this.form2.data.payBy === null) {
if (this.form2.data.payBy === undefined || this.form2.data
.payBy === '' || this.form2.data.payBy === null) {
vk.toast('请选择费用承担方')
this.form2.props.loading = false
return false
@@ -924,7 +997,9 @@ export default {
this.form2.props.loading = false
return false
}
const res = await this.$confirm(`1、您确认,若全体各签约方未完成全部签约流程,本签约文件不发生法律效力,系统无法对该签约文件予以保存及提供司法调证服务;<br>2、您确认,当前签约的实名主体名称、手机号码与注册认证阶段提交的信息未发生变更;若发生变更,必须及时进行重新认证后方可继续签约,避免产生严重法律风险与经济损失;<br>3、您确认,本次签约为本系统实名认证主体操作,如若不是实名认证主体操作,授权委托人需本系统进行授权认证后签约;<br>4、您确认,您与其他签约方均系在平等、自愿的基础上完成本次签约;<br>5、你确认,此次签约内容符合《中华人民共和国电子签名法》《民法典》生效保护范畴;<br>6、如因法律法规、监管部门政策更新等原因,系统提示您重新进行实名认证、证书续期等重要提示时,请您必须在签约前完成,否则造成任何损失,平台不承担任何责任;以上签约条件您均已经知悉并确认,否则您有权拒绝本次签约。`, '提示', {
const res = await this.$confirm(
`1、您确认,若全体各签约方未完成全部签约流程,本签约文件不发生法律效力,系统无法对该签约文件予以保存及提供司法调证服务;<br>2、您确认,当前签约的实名主体名称、手机号码与注册认证阶段提交的信息未发生变更;若发生变更,必须及时进行重新认证后方可继续签约,避免产生严重法律风险与经济损失;<br>3、您确认,本次签约为本系统实名认证主体操作,如若不是实名认证主体操作,授权委托人需本系统进行授权认证后签约;<br>4、您确认,您与其他签约方均系在平等、自愿的基础上完成本次签约;<br>5、你确认,此次签约内容符合《中华人民共和国电子签名法》《民法典》生效保护范畴;<br>6、如因法律法规、监管部门政策更新等原因,系统提示您重新进行实名认证、证书续期等重要提示时,请您必须在签约前完成,否则造成任何损失,平台不承担任何责任;以上签约条件您均已经知悉并确认,否则您有权拒绝本次签约。`,
'提示', {
confirmButtonText: '知悉同意',
showCancelButton: false,
type: 'warning',
@@ -935,7 +1010,9 @@ export default {
url: 'client/contract/kh/addSign',
data: this.form2.data
})
vk.redirectTo({url: '/pages/contract/index?tab=1'})
vk.redirectTo({
url: '/pages/contract/index?tab=1'
})
}
}
console.log('complete:', pdfRes)
@@ -945,13 +1022,11 @@ export default {
},
// 表单字段显示规则
columns: [
{
columns: [{
key: "signType",
title: "",
type: "radio",
data: [
{
data: [{
value: '1',
label: "单据签约"
},
@@ -983,8 +1058,7 @@ export default {
key: "payBy",
title: "签约费用",
type: "radio",
data: [
{
data: [{
value: 0,
label: "发起方承担"
},
@@ -1023,8 +1097,7 @@ export default {
key: "userType",
title: "对方身份",
type: "select",
data: [
{
data: [{
value: 1,
label: "买家"
},
@@ -1175,7 +1248,9 @@ export default {
this.formData.status = Number(options.tab)
this.isSign = false
if (options.result_desc) {
uni.showLoading({title: '请求中...'})
uni.showLoading({
title: '请求中...'
})
setTimeout(async () => {
await this.query(1)
uni.hideLoading()
@@ -1296,8 +1371,47 @@ export default {
this.isSignTips = false
this.menuIndex = ''
},
// AI 客服方法
chatQuickSend(text) {
this.chatInput = text;
this.chatSend();
},
async chatSend() {
const msg = this.chatInput.trim();
if (!msg || this.chatLoading) return;
this.chatInput = '';
this.chatMessages.push({ role: 'user', content: msg });
this.chatLoading = true;
this.chatScrollToBottom();
try {
const res = await vk.callFunction({
url: 'client/ai/pub/chat',
data: { message: msg, bot_id: '7683509749475819554' }
});
if (res.data.conversation_id) this.chatConversationId = res.data.conversation_id;
this.chatMessages.push({ role: 'assistant', content: res.data.answer });
} catch (e) {
this.chatMessages.push({ role: 'assistant', content: '网络异常,请稍后重试' });
}
this.chatLoading = false;
this.chatScrollToBottom();
},
chatScrollToBottom() {
setTimeout(() => {
if (this.chatMessages.length === 0) {
this.chatScrollId = 'chat-welcome';
} else if (this.chatLoading) {
this.chatScrollId = 'chat-loading';
} else {
this.chatScrollId = 'chat-msg' + (this.chatMessages.length - 1);
}
}, 100);
},
changeAuthInfo() {
vk.navigateTo({url: '/pages/user/user?menu=fadada'})
vk.navigateTo({
url: '/pages/user/user?menu=fadada'
})
},
async getSignFlow() {
const res = await vk.callFunction({
@@ -1339,7 +1453,9 @@ export default {
},
async changeContract(contract) {
this.form1.data.contractId = contract._id
const {data} = await vk.callFunction({
const {
data
} = await vk.callFunction({
url: 'client/contract/kh/getDetail',
title: '请求中...',
data: {
@@ -1379,7 +1495,9 @@ export default {
// this.$refs.form2.resetForm()
// this.$refs.form1.resetForm()
// this.query(1)
vk.redirectTo({url: '/pages/contract/index?tab=1'})
vk.redirectTo({
url: '/pages/contract/index?tab=1'
})
},
/**
* 签署合同
@@ -1389,7 +1507,9 @@ export default {
async extSign(sign, contract) {
if (sign) return
try {
await this.$confirm(`1、您确认,若全体各签约方未完成全部签约流程,本签约文件不发生法律效力,系统无法对该签约文件予以保存及提供司法调证服务;<br>2、您确认,当前签约的实名主体名称、手机号码与注册认证阶段提交的信息未发生变更;若发生变更,必须及时进行重新认证后方可继续签约,避免产生严重法律风险与经济损失;<br>3、您确认,本次签约为本系统实名认证主体操作,如若不是实名认证主体操作,授权委托人需本系统进行授权认证后签约;<br>4、您确认,您与其他签约方均系在平等、自愿的基础上完成本次签约;<br>5、你确认,此次签约内容符合《中华人民共和国电子签名法》《民法典》生效保护范畴;<br>6、如因法律法规、监管部门政策更新等原因,系统提示您重新进行实名认证、证书续期等重要提示时,请您必须在签约前完成,否则造成任何损失,平台不承担任何责任;以上签约条件您均已经知悉并确认,否则您有权拒绝本次签约。`, '提示', {
await this.$confirm(
`1、您确认,若全体各签约方未完成全部签约流程,本签约文件不发生法律效力,系统无法对该签约文件予以保存及提供司法调证服务;<br>2、您确认,当前签约的实名主体名称、手机号码与注册认证阶段提交的信息未发生变更;若发生变更,必须及时进行重新认证后方可继续签约,避免产生严重法律风险与经济损失;<br>3、您确认,本次签约为本系统实名认证主体操作,如若不是实名认证主体操作,授权委托人需本系统进行授权认证后签约;<br>4、您确认,您与其他签约方均系在平等、自愿的基础上完成本次签约;<br>5、你确认,此次签约内容符合《中华人民共和国电子签名法》《民法典》生效保护范畴;<br>6、如因法律法规、监管部门政策更新等原因,系统提示您重新进行实名认证、证书续期等重要提示时,请您必须在签约前完成,否则造成任何损失,平台不承担任何责任;以上签约条件您均已经知悉并确认,否则您有权拒绝本次签约。`,
'提示', {
confirmButtonText: '知悉同意',
showCancelButton: false,
type: 'warning',
@@ -1463,8 +1583,10 @@ export default {
// })
console.log(123123)
let content = ''
const buyer = userContract.contract.partyList.find(item => item.role !== 'third' && userContract.contract.userId === item.userId)
const seller = userContract.contract.partyList.find(item => item.role !== 'third' && userContract.contract.userId !== item.userId)
const buyer = userContract.contract.partyList.find(item => item.role !== 'third' && userContract.contract
.userId === item.userId)
const seller = userContract.contract.partyList.find(item => item.role !== 'third' && userContract.contract
.userId !== item.userId)
const thirdList = userContract.contract.partyList.filter(item => item.role === 'third')
content += `<view class="flex flex-col justify-start items-start py-10 border-bottom">
<view>身份:发起方</view>
@@ -1519,7 +1641,8 @@ export default {
let openUrl = url
if (Array.isArray(url)) openUrl = url[url.length - 1]
if (!openUrl) return
window.open(`https://www.jinjiuxinzg.cn/pdfjs/web/viewer.html?file=${encodeURIComponent(openUrl)}`, '_blank')
window.open(`https://www.jinjiuxinzg.cn/pdfjs/web/viewer.html?file=${encodeURIComponent(openUrl)}`,
'_blank')
},
downloadContractTemplate() {
window.open(this.contractTemplate)
@@ -1581,7 +1704,8 @@ export default {
if (contract.certificate.deliveryMethods.paper) {
vk.toast('您的司法调证文件已发货,请注意查收')
} else {
if (contract.certificate.downloadUrl) window.open(contract.certificate.downloadUrl)
if (contract.certificate.downloadUrl) window.open(contract.certificate
.downloadUrl)
else vk.toast('文件生成中,请耐心等待')
}
} else vk.toast('操作失败,请联系客服')
@@ -1590,7 +1714,10 @@ export default {
}
},
async downloadContract(contract_id) {
let loadingInstance1 = Loading.service({fullscreen: true, text: '加载中...'});
let loadingInstance1 = Loading.service({
fullscreen: true,
text: '加载中...'
});
const res = await vk.callFunction({
url: 'client/contract/pub/getDownload',
data: {
@@ -1599,12 +1726,16 @@ export default {
});
loadingInstance1.close()
if (res) {
window.open(`https://textapi.fadada.com/api2/downLoadContract.api?app_id=502704&timestamp=${res.data.timestamp}&v=2.0&msg_digest=${res.data.msg_digest}&contract_id=${contract_id}`)
window.open(
`https://textapi.fadada.com/api2/downLoadContract.api?app_id=502704&timestamp=${res.data.timestamp}&v=2.0&msg_digest=${res.data.msg_digest}&contract_id=${contract_id}`
)
}
},
getUserInfo() {
vk.userCenter.getCurrentUserInfo({
success: ({userInfo}) => {
success: ({
userInfo
}) => {
this.userInfo = userInfo
if (!this.userInfo.mobile) {
this.$confirm('请先绑定手机号', '提示', {
@@ -1612,9 +1743,11 @@ export default {
showCancelButton: false,
type: 'warning'
}).then(() => {
vk.redirectTo(`/pages/auth/login/wechat?state=jjx_` + this.userInfo.accountType)
vk.redirectTo(`/pages/auth/login/wechat?state=jjx_` + this.userInfo
.accountType)
}).catch(() => {
vk.redirectTo(`/pages/auth/login/wechat?state=jjx_` + this.userInfo.accountType)
vk.redirectTo(`/pages/auth/login/wechat?state=jjx_` + this.userInfo
.accountType)
})
return
}
@@ -1627,11 +1760,14 @@ export default {
closeOnClickModal: false,
closeOnPressEscape: false
}).then(() => {
vk.navigateTo({url: '/pages/user/user?menu=fadada'});
vk.navigateTo({
url: '/pages/user/user?menu=fadada'
});
})
}
this.invoiceForm.phone = this.userInfo.mobile
this.invoiceForm.email = this.userInfo.businessCard ? this.userInfo.businessCard.email : ''
this.invoiceForm.email = this.userInfo.businessCard ? this.userInfo.businessCard
.email : ''
if (this.userInfo.accountType === 2) {
if (this.userInfo.fadadaConfirm && this.userInfo.company) {
this.invoiceForm.name = this.userInfo.company.companyName
@@ -1651,7 +1787,9 @@ export default {
});
},
async getQybPackage() {
const {rows} = await vk.callFunction({
const {
rows
} = await vk.callFunction({
url: 'client/qyb/list/pub/getList',
data: {
pageIndex: 1,
@@ -1661,7 +1799,9 @@ export default {
this.qybPackageList = rows
},
async doBuyQyb() {
const {data} = await vk.callFunction({
const {
data
} = await vk.callFunction({
url: 'client/qyb/list/kh/buy',
title: '请求中...',
data: {
@@ -1899,4 +2039,130 @@ export default {
background-color: #f1c7c7;
color: #333;
}
/* ===== 右侧悬浮工具栏 ===== */
.float-toolbar {
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
padding: 12px 0;
z-index: 100;
width: 72px;
}
.float-toolbar .tool-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 8px;
cursor: pointer;
transition: all 0.2s;
}
.float-toolbar .tool-item:hover {
background: #f6eae9;
}
.float-toolbar .tool-item .tool-icon {
font-size: 22px;
margin-bottom: 4px;
}
.float-toolbar .tool-item .tool-label {
font-size: 10px;
color: #666;
text-align: center;
line-height: 1.3;
}
.float-toolbar .tool-divider {
height: 1px;
background: #eee;
margin: 4px 12px;
}
.float-toolbar .tool-item.ai-hot {
position: relative;
}
.float-toolbar .tool-item.ai-hot::after {
content: "AI";
position: absolute;
top: 4px;
right: 8px;
background: #a92924;
color: #fff;
font-size: 8px;
padding: 1px 4px;
border-radius: 6px;
}
/* ===== 新增AI功能菜单组 ===== */
.menu-group-title {
padding: 8px 20px 4px;
font-size: 11px;
color: #999;
letter-spacing: 2px;
text-transform: uppercase;
}
/* AI 客服嵌入样式 */
.ai-chat-embed {
background: #fff;
border-radius: 8px;
display: flex;
flex-direction: column;
height: calc(100vh - 260px);
overflow: hidden;
}
.chat-header-bar {
padding: 14px 20px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-title { font-size: 15px; font-weight: 600; }
.chat-subtitle { font-size: 12px; color: #999; }
.chat-msg-list { flex: 1; padding: 16px 20px; background: #f7f8fa; }
.chat-row { display: flex; gap: 10px; margin-bottom: 12px; }
.chat-row.right { flex-direction: row-reverse; }
.chat-avatar {
width: 32px; height: 32px; border-radius: 50%;
background: linear-gradient(135deg, #a92924, #d4574e);
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 14px; flex-shrink: 0;
}
.chat-bubble {
max-width: 65%; padding: 10px 14px; border-radius: 12px;
font-size: 13px; line-height: 1.6; word-break: break-word;
}
.chat-row:not(.right) .chat-bubble { background: #fff; border: 1px solid #eee; border-top-left-radius: 4px; }
.chat-row.right .chat-bubble { background: #a92924; color: #fff; border-top-right-radius: 4px; }
.chat-bubble.typing { color: #999; }
.quick-btns { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.quick-btn {
padding: 5px 12px; border-radius: 14px; font-size: 12px; cursor: pointer;
border: 1px solid #a92924; color: #a92924; background: #fff; transition: all 0.2s;
}
.quick-btn:hover { background: #a92924; color: #fff; }
.chat-input-bar {
padding: 12px 20px; border-top: 1px solid #eee;
display: flex; gap: 10px; background: #fff;
}
.chat-input-bar input {
flex: 1; height: 38px; border: 1px solid #ddd; border-radius: 8px;
padding: 0 12px; font-size: 13px; outline: none;
}
.chat-input-bar input:focus { border-color: #a92924; }
.chat-send-btn {
padding: 0 20px; height: 38px; line-height: 38px;
background: #a92924; color: #fff; border-radius: 8px;
font-size: 13px; cursor: pointer;
}
.chat-send-btn:hover { background: #982520; }
</style>