新增ai问答逻辑

This commit is contained in:
yanghang
2025-07-16 16:47:52 +08:00
parent 0e7b323296
commit 216fe5f846
9 changed files with 424 additions and 68 deletions
@@ -40,7 +40,7 @@
},
mode:{
type: String,
default: 'widthFix'
default: 'aspectFit'
},
previewDetail: {
default: '',
+3 -3
View File
@@ -587,9 +587,9 @@ const initsocketTask = () => {
getGraphInfoUnStudy(1)
}
}else{
if(['0005', 'QQ0005'].includes(res.rtnCode)) {
goto_login_fun();
}
// if(['0005', 'QQ0005'].includes(res.rtnCode)) {
// goto_login_fun();
// }
}
})
}
+148 -44
View File
@@ -1,55 +1,66 @@
<template>
<view class="answer-content">
<MarkdownPreview :mdString="mdText"/>
<view class="talking-cont">
<MarkdownPreview :mdString="mdText"/>
</view>
<view class="btns-cont" v-if="false">
<view class="replay-btn" @click="reAnswer">
<image class="icon" src="@/static/images/dialog/answer-restart.png" style="width: 100%;height: 100%;"></image>
</view>
<view class="replay-btn" @click="stopAnswer">
<image class="icon" src="@/static/images/dialog/answer-stop.png" style="width: 100%;height: 100%;"></image>
</view>
<!-- <view class="replay-btn" @click="zanAnswer">
<image
class="icon"
src="@/static/images/dialog/answer-zan.png"
style="width: 100%;height: 100%;"
></image>
<image
class="icon"
src="@/static/images/dialog/answer-yizan.png"
style="width: 100%;height: 100%;"
></image>
</view>
<view class="play-btn">
<image
class="icon"
src="@/static/images/dialog/answer-fankui.png"
style="width: 100%;height: 100%;"
></image>
<image
class="icon"
src="@/static/images/dialog/answer-yifankui.png"
style="width: 100%;height: 100%;"
></image>
</view> -->
</view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { onMounted, computed, ref } from 'vue'
import MarkdownPreview from '@/components/markdown-preview/markdown-preview.vue';
const mdText = ref('')
const baseText = ref(`#### 在客服交流中使用标准术语是提高专业形象和服务质量的重要手段。以下是一些常见的客服标准用语及其适用场景:
const emit = defineEmits(['handleEvents'])
const props = defineProps({
dataInfo: {
default: () => ({}),
type: Object
},
})
const mdText = computed(() => props.dataInfo?.talkingText)
1. **问候与介绍:**
- “您好,欢迎来到[公司名称],我是您的客服代表[姓名],很高兴为您服务。”
- “早上好/下午好/晚上好,[公司名称]客服中心,请问有什么可以帮到您的吗?”
2. **确认信息:**
- “为了更好地为您提供服务,能否麻烦您提供一下[具体需要的信息,如订单号、会员账号等]?”
- “我来确认一下,您的问题是关于[问题的具体描述],对吗?”
3. **表达理解与同情:**
- “非常理解您的感受,遇到这种情况确实会让人感到不便/烦恼。”
- “感谢您的耐心等待,我们正在积极处理您的问题。”
4. **解决问题:**
- “针对您提到的问题,我们可以采取[解决方案]的方式解决,您看这样可以吗?”
- “我已经记录了您的反馈,并将尽快转交给相关部门处理,预计[解决时间]内会有结果。”
5. **结束对话:**
- “如果您没有其他问题的话,那我们就先这样吧,祝您生活愉快!”
- “再次感谢您的来电,期待下次为您服务。再见!”
6. **特殊情况处理:**
- “很抱歉给您带来了不愉快的体验,这并不是我们希望发生的事情。我们会立即调查此事,并确保类似情况不再发生。”
- “对于您所反映的情况,我们深感歉意,但根据我们的规定,[解释原因]。不过,我可以尝试帮您寻找其他的解决方案。”
以上是客服交流中常用的标准化语言,实际应用时可根据具体情况适当调整,以保持沟通的真实性和亲和力。`)
let start = 0
let length = baseText.length
const addText = () => {
let _random = Math.ceil(Math.random()*5)
let _str = baseText.value.substr(start,_random)
start += _random
mdText.value += _str
setTimeout(()=>{
addText()
},200)
}
onMounted(()=>{
addText()
})
const reAnswer = () => {
emit('handleEvents', 'reAnswer')
}
const stopAnswer = () => {
emit('handleEvents', 'stop')
}
const zanAnswer = () => {
emit('handleEvents', 'great')
}
</script>
<style scoped lang="scss">
@@ -57,7 +68,100 @@ onMounted(()=>{
background-color: #fff;
padding: 20rpx;
border-radius: 15rpx;
padding-bottom: 30rpx;
padding-bottom: 10rpx;
position: relative;
.talking-cont{
padding-bottom: 20rpx;
}
.btns-cont{
overflow: hidden;
border-top: 3rpx solid #eee;
.replay-btn{
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
margin-right: 16rpx;
margin-top: 25rpx;
}
.play-btn{
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
margin-right: 16rpx;
margin-top: 25rpx;
}
.next-learn-btn{
float: left;
padding: 0 60rpx 0 20rpx;
height: 62rpx;
line-height: 62rpx;
font-size: 30rpx;
background-color: #06f;
color: #fff;
border-radius: 8rpx;
margin-top: 16rpx;
margin-right: 16rpx;
position: relative;
&.replay-study-btn{
float: right;
}
&.restart{
background-color: #eaf2ff;
color: #06f;
}
.icon-iamge{
width: 25rpx;
height: 25rpx;
position: absolute;
right: 20rpx;
top: 50%;
margin-top: -12rpx;
}
}
.replay-learn-btn{
float: left;
padding: 0 60rpx 0 20rpx;
height: 62rpx;
line-height: 62rpx;
font-size: 30rpx;
color: #06f;
background-color: #eaf2ff;
border-radius: 8rpx;
margin-top: 16rpx;
margin-right: 16rpx;
position: relative;
.icon-iamge{
width: 25rpx;
height: 25rpx;
position: absolute;
right: 20rpx;
top: 50%;
margin-top: -12rpx;
}
}
.next-btn{
float: right;
padding: 0 60rpx 0 20rpx;
height: 62rpx;
line-height: 62rpx;
font-size: 30rpx;
background-color: #06f;
color: #fff;
border-radius: 8rpx;
margin-top: 16rpx;
position: relative;
.icon-iamge{
width: 25rpx;
height: 25rpx;
position: absolute;
right: 20rpx;
top: 50%;
margin-top: -12rpx;
}
}
}
}
</style>
+4 -3
View File
@@ -25,7 +25,7 @@
</template>
<script setup>
import { computed, ref, toRefs } from "vue"
import { computed, ref, toRefs, watch } from "vue"
const props = defineProps({
dataInfo: {
default: () => ({}),
@@ -37,12 +37,13 @@
},
})
const { activeVoiceId, dataInfo } = toRefs(props)
watch(() => props.dataInfo, () => {},{deep: true,immediate: true})
const voiceLoading = ref(false)
const translateLoading =ref(false)
const translateLoading = ref(false)
const voiceTime = ref('')
const showContent = computed(()=>{
return '问题'
return dataInfo.value.talkingText || ''
})
</script>
+18 -4
View File
@@ -1,22 +1,36 @@
<template>
<view class="talking-item">
<questionVue v-if="talkingType === 'question'"></questionVue>
<answerVue v-else-if="talkingType === 'answer'"></answerVue>
<questionVue v-if="talkingType === 'question'" :dataInfo="talkingInfo"></questionVue>
<answerVue
v-else-if="talkingType === 'answer'"
:dataInfo="props.talkingInfo"
@handleEvents="handleEvents"
></answerVue>
<view v-else></view>
</view>
</template>
<script setup>
import { toRefs } from 'vue'
import { toRefs, watch } from 'vue'
import questionVue from './question.vue';
import answerVue from './answer.vue';
const emit = defineEmits(['handleEvents'])
const props = defineProps({
talkingType:{
default:'question',
type: String
},
talkingInfo:{
default: () => ({}),
type: Object
}
})
const { talkingType } = toRefs(props)
watch(() => props.talkingInfo, () => {},{deep: true,immediate: true})
const { talkingType } = toRefs(props)
const handleEvents = (type) => {
emit('handleEvents', type, props.talkingInfo)
}
</script>
<style >
+248 -11
View File
@@ -5,8 +5,8 @@
<view class="bot-bg"></view>
<scroll-view class="index-dialog-body" :scroll-y="true" upper-threshold="0" refresher-enabled="true" refresher-default-style="none" @refresherrefresh="scrolltolower">
<view class="seek-setting">
设置
<image src="@/static/images/course/setting.png" alt="" class="image-icon"></image>
<image src="@/static/images/dialog/new-dialog.png" alt="" class="image-icon-new"></image>
<image src="@/static/images/dialog/history.png" alt="" class="image-icon"></image>
</view>
<view class="body-title">
<uv-icon class="arrow-icon" name="arrow-left" @click="common.navigateBack()"/>
@@ -23,10 +23,18 @@
<view class="questions-view">
<scroll-view :scroll-x="true" :show-scrollbar="false">
<view class="question-row">
<view class="question-item" v-for="item in Math.ceil(questionList.length/2)">{{questionList[item-1]}}</view>
<view
class="question-item"
v-for="item in Math.ceil(questionList.length/2)"
@click="sendText(questionList[item-1])"
>{{questionList[item-1]}}</view>
</view>
<view class="question-row">
<view class="question-item" v-for="item in questionList.length - Math.ceil(questionList.length/2)">{{questionList[item + Math.ceil(questionList.length/2)-1]}}</view>
<view
class="question-item"
v-for="item in questionList.length - Math.ceil(questionList.length/2)"
@click="sendText(questionList[item + Math.ceil(questionList.length/2)-1])"
>{{questionList[item + Math.ceil(questionList.length/2)-1]}}</view>
</view>
</scroll-view>
</view>
@@ -39,8 +47,10 @@
:scroll-with-animation="true"
>
<TalkingItem
v-for="item in 2"
:talkingType="item%2===1?'question':'answer'"
v-for="item in talkingList"
:talkingType="item.type"
:talkingInfo="item"
@handleEvents="talkItemEvents"
>
</TalkingItem>
</scroll-view>
@@ -89,10 +99,19 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, unref, onMounted, watch, nextTick } from 'vue'
import common from '@/common/common';
import TouchBtn from '@/pages/course/components/touchBtn.vue'
import TalkingItem from './components/talking-item.vue';
import { useSocketStore } from "@/store/socket.js"
import { useStorageStore } from "@/store/storage.js"
import { storeToRefs } from 'pinia'
import { onLoad, onShow, onUnload, onHide } from '@dcloudio/uni-app';
import { get_base_url, goto_login_fun } from '@/api/request'
const socketStore = useSocketStore()
const { SocketObj } = storeToRefs(socketStore)
const questionList = ref([
'对公小程序开户流程?',
'厅堂服务经理2025年管理办法?',
@@ -107,7 +126,7 @@
])
const scrollBoxRef = ref()
const scrollTop = ref(99999)
const scrollToBottomNow = (time = 500) => {
const scrollToBottomNow = (time = 100) => {
nextTick(()=>{
setTimeout(() => {
scrollTop.value += 1
@@ -122,6 +141,175 @@
},time)
})
}
// ws 初始化逻辑
const reconnectTimer = null // 重连timer
const isManualClose = ref(false) // 是否手动关闭
const reqBatch = ref('')
const seqNo = ref('')
const mascotId = ref('Tst00001')
const answerText = ref('')
const answerIsOver = ref(true)
const talkingList = ref([])
const initsocketTask = () => {
socketStore.creatSocket('/traask/asksocket/open?summary=')
unref(SocketObj).on('open', (res)=>{
if(!answerIsOver.value){
sendMessage({
"commond" : 'ASK-RE-START',
"body" : {
reqBatch: reqBatch.value,
seqNo: seqNo.value,
answerContent: answerText.value
}
})
}
})
unref(SocketObj).on('message', (res)=>{
const { rtnCode, body, commond } = JSON.parse(res.data)
console.log(rtnCode, body, commond)
if(rtnCode === '0000'){
if(commond === 'ASK-OPEN'){
console.log('链接成功!')
sendMessage({
"commond" : 'ASK-START',
"body" : {
mascotId: mascotId.value,
deviceImei: ''
}
})
return
}
if(commond === 'ASK-START'){
console.log('开启对话!')
reqBatch.value = body.reqBatch
return
}
if(commond === 'ASK'){
console.log('body')
seqNo.value = body.seqNo
touchBtnCallBack.value && touchBtnCallBack.value()
touchBtnCallBack.value = null
talkingList.value.push({
type: 'question',
talkingText: body.intrctContent,
...body
})
return
}
if(commond === 'ASK-STOP'){
console.log('停止回答标记!')
stopAnswerCallBack.value && stopAnswerCallBack.value(askData.value)
setTimeout(()=>{
stopAnswerCallBack.value = null
},10)
return
}
if(commond === 'ASK-ANSWER'){
console.log('开始回答!')
if(body.event === 'start'){
answerIsOver.value = false
answerText.value = ''
talkingList.value.push({
type: 'answer',
talkingText: answerText.value,
...body
})
}else if(body.event === 'token'){
answerIsOver.value = false
answerText.value += body.data
}else if(body.event === 'end'){
answerIsOver.value = true
}
return
}
if(commond === 'ASK-RE-ANSWER'){
console.log('开始继续回答!')
if(body.event === 'start'){
answerIsOver.value = false
answerText.value = ''
}else if(body.event === 'token'){
answerIsOver.value = false
answerText.value = body.data
}else if(body.event === 'end'){
answerIsOver.value = true
}
return
}
if(commond === 'ASK-ERR'){
console.log('操作异常!')
console.log(body)
return
}
}else{
if(['0005', 'QQ0005'].includes(res.rtnCode)) {
goto_login_fun();
}
}
})
watchFlag.value += 1
}
const reconnect = ()=> {
if(reconnectTimer || isManualClose.value) return
reconnectTimer = setTimeout(()=>{
initsocketTask()
},3000)
}
const sendMessage = (data) => {
unref(SocketObj).send(data)
}
watch(() => answerText.value,(val) => {
if(val){
changeText(val)
scrollToBottomNow()
}
})
const changeText = () => {
if(talkingList.value.length === 0) return
talkingList.value[talkingList.value.length-1].talkingText = answerText.value
}
const stopAnswerCallBack = ref(null)
const talkItemEvents = (type, data) => {
switch (type){
case 'reAnswer':
console.log('重新获取数据')
// 设置停止回调
stopAnswerCallBack.value = ($data) => {
sendMessage({
"commond" : 'ASK',
"body" : $data
})
}
// 停止获取
sendMessage({
"commond" : 'ASK-STOP',
"body" : {
reqBatch: reqBatch.value,
seqNo: seqNo.value,
}
})
break;
case 'stop':
console.log('停止获取数据')
stopAnswerCallBack.value = null
// ASK-STOP
sendMessage({
"commond" : 'ASK-STOP',
"body" : {
reqBatch: reqBatch.value,
seqNo: seqNo.value,
}
})
break;
default:
break;
}
}
const routerTo = (type) => {
switch(type){
case 'AI学':
@@ -136,15 +324,57 @@
common.switchTab('/pages/index/index')
}
const uploadRecordNow = (voicePath) => {
// /traask/traAskchat/voiceTrans
}
const touchBtnCallBack = ref(null)
const askData = ref({})
const sendText = (item) => {
submitAnswerNow(item)
}
// 发送文本
const submitAnswerNow = (val, cb) => {
const submitAnswerNow = (val, cb = null) => {
if(!answerIsOver.value){
// 问题回答中
return
}
touchBtnCallBack.value = cb
askData.value = {
reqBatch: reqBatch.value,
intrctWay: '01',//01-文本;02-语音;03-图片
intrctContent: val,
bucketKey:''
}
sendMessage({
"commond" : 'ASK',
"body" : askData.value
})
}
const startRecordCallback = () => {
// 开始录音回调
}
const watchFlag = ref(1)
onMounted(()=>{
initsocketTask()
})
onHide(()=>{
if(watchFlag.value === 1) return
// 停止获取
sendMessage({
"commond" : 'ASK-STOP',
"body" : {
reqBatch: reqBatch.value,
seqNo: seqNo.value,
}
})
socketStore?.closeSocket()
})
onShow(()=>{
if(watchFlag.value === 1) return
socketStore?.createSocket()
})
onUnload(()=>{
socketStore?.closeSocket()
})
</script>
<style scoped lang="scss">
@@ -215,6 +445,13 @@
right: 0;
top: 10rpx;
}
.image-icon-new{
right: 50rpx;
width: 30rpx;
height: 32rpx;
position: absolute;
top: 10rpx;
}
}
.ai-info{
Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B