最近接到了个需求,要做一个智能客服的H5页面,并且不需要接入人工,只需要将用户的问题返回给接口,再从接口获取回答显示在聊天页面上。
在此参考了小瓶子大佬的回答,非常感谢:点此跳转;
我在此页面的基础上改了一下回答逻辑,并加了一个保存本地聊天记录的功能,后续可能还要开发返回视频or图片url显示的功能,有的话再补充。
PS:我的需求是要用在移动端,所以做了移动端适配,不过感觉做的不是很好,有其他想法欢迎一起讨论,同时我的api需要传入userid,所以用url传参传入了userid,不需要的可以自己去掉。
效果图:
以下是我的代码:
<template>
<div class="main" >
<div class="box" >
<div style=" text-align: center;" class="title">
<!-- <img src="" alt class="logo" /> -->
<!-- <span style="font-size: 40rpx; text-align: center;" class="title-hn">智能客服</span> -->
</div>
<div id="content" >
<div class="history_button" style="background-color: #f5f5f5;" v-if="isShow">
<button style=" text-align: center; color: #bcbcbc; background-color: #f5f5f5;" @click="getHistory">查看历史记录</button>
</div>
<div v-for="(item,index) in info" :key="index">
<div class="info_r info_default" v-if="item.type == 'leftinfo'">
<span class="circle circle_r">
<img src class="pic_r" src="/static/images/kefu2.png"></img>
</span>
<div class="time_r">{{item.time}}</div>
<div class="con_r con_text">
<div v-html="formatContent(item.content)"></div>
</div>
</div>
<div class="info_l" v-if="item.type == 'rightinfo'">
<div class="time_l">{{item.time}}</div>
<div class="con_r con_text">
<span class="con_l">{{item.content}}</span>
</div>
</div>
</div>
</div>
<div class="setproblem">
<textarea
placeholder="请输入您的问题..."
id="text"
v-model="customerText"
@keyup.enter="sentMsg()"
@focus="scrollToView"
></textarea>
<button @click="sentMsg()" class="setproblems">
<span style="vertical-align: 8px;font-size: 30rpx;">发 送</span>
</button>
</div>
</div>
</div>
</template>
<script>
import {
getSign
} from '@/utils/sign.js';
export default {
name: "onlineCustomer",
components: {},
computed: {},
data() {
return {
userId: null,
customerText: "",
isShow: true,
Sending: true,
getHistoryId: "",
info: [
{
type: "leftinfo",
time: this.getTodayTime(),
name: "robot",
content:
"您好,请说出您的问题,小乐可以更快给你解答哦~",
question: [],
},
],
timer: null,
keyboardHeight: 0,
};
},
created() {
this.showTimer();
},
onLoad() {
// userId = this.GetRequest();
// console.log("userId = "+userId);
this.GetRequest();
},
mounted() {
// this.showTimer();
uni.onKeyboardHeightChange(res => {
this.keyboardHeight = res.height;
});
window.addEventListener('resize', this.handleResize);
// 确保文本可以选择
// document.addEventListener('selectstart', function (e) {
// e.stopPropagation();
// });
// const selectableElements = document.querySelectorAll('.con_text, #content, body');
// selectableElements.forEach(element => {
// element.style.userSelect = 'text';
// element.style.webkitUserSelect = 'text';
// element.style.msUserSelect = 'text';
// element.style.mozUserSelect = 'text';
// });
},
beforeDestroy() {
uni.offKeyboardHeightChange();
window.removeEventListener('resize', this.handleResize);
},
watch: {
keyboardHeight(newHeight) {
this.onKeyboardShow(300);
if (newHeight > 0) {
// this.onKeyboardShow(230);
} else {
this.onKeyboardHide();
}
}
},
methods: {
///换行符、加粗符更换为适配html
formatContent(content) {
// 处理换行符
let formattedContent = content.replace(/\n/g, '<br>');
// 处理加粗
formattedContent = formattedContent.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
return formattedContent;
},
handleResize() {
this.keyboardHeight = 0; // 重置键盘高度
},
onKeyboardShow(height) {
this.$nextTick(() => {
const content = document.getElementById('content');
content.style.paddingBottom = `${height}rpx`;
content.scrollTop = content.scrollHeight;
});
},
onKeyboardHide() {
this.$nextTick(() => {
const content = document.getElementById('content');
content.style.paddingBottom = '0px';
});
},
scrollToView() {
this.$nextTick(() => {
document.getElementById('text').scrollIntoView({ behavior: 'smooth', block: 'center' });
});
},
//获取历史数据
getHistory() {
var getHistory = uni.getStorageSync(this.getHistoryId);
if(!getHistory){
uni.showToast({
title: '暂无历史记录', // 提示的内容
icon: 'none', // 提示的图标,有效值: "success", "loading", "none"
duration: 2000 // 提示的持续时间,单位毫秒
});
return;
}
console.log('获取历史数据');
this.info = [];
console.log(getHistory);
var i = 0;
for(i; i < getHistory.length; i++){
this.info.push(getHistory[i]);
}
// console.log(getHistory);
this.isShow = false;
},
// 用户发送消息
sentMsg() {
clearTimeout(this.timer);
this.showTimer();
let text = this.customerText.trim();
if(!this.Sending){
uni.showToast({
title: '客官别着急', // 提示的内容
icon: 'none', // 提示的图标,有效值: "success", "loading", "none"
duration: 2000 // 提示的持续时间,单位毫秒
});
return;
}
if (text != "") {
var obj = {
type: "rightinfo",
time: this.getTodayTime(),
content: text,
};
this.info.push(obj);
this.saveChatRecord(obj);
this.Sending = false; //不能两个消息同时发
this.appendRobotMsg(this.customerText);
this.customerText = "";
this.$nextTick(() => {
var contentHeight = document.getElementById("content");
contentHeight.scrollTop = contentHeight.scrollHeight;
});
}
},
// 机器人回答消息
appendRobotMsg(text) {
clearTimeout(this.timer);
this.showTimer();
text = text.trim();
console.log(text);
console.log('this.userId = '+ this.userId);
var data = {
"aiId": this.userId,
// "aiId": 123,
"userQuestion": text,
// "messageId": 123,
}
var x = getSign(data)
let obj2 = {
type: "leftinfo",
time: this.getTodayTime(),
name: "robot",
content: '正在输入中...',
question: [],
};
this.info.push(obj2);
uni.request({
url:'/ai/st_dyna/wx/server/aiService.do',
method:'POST',
header:{
// 'Content-Type': 'application/json'
'Content-Type': 'application/x-www-form-urlencoded'
},
data: x,
success: (res) => {
this.info.pop(obj2);
if(res.data['msg'] == "0" ){
let obj3 = {
type: "leftinfo",
time: this.getTodayTime(),
name: "robot",
content: "您可以在描述清晰一点吗?",
question: [],
};
this.info.push(obj3);
}else if(res.data['code'] == 400){
let obj4 = {
type: "leftinfo",
time: this.getTodayTime(),
name: "robot",
content: '服务器开小差了,请稍后再试',
question: [],
};
this.info.push(obj4);
}else{
let obj = {
type: "leftinfo",
time: this.getTodayTime(),
name: "robot",
content: res.data['msg'],
question: [],
};
this.info.push(obj);
this.saveChatRecord(obj);
}
this.Sending = true;
this.$nextTick(() => { //滚动聊天到底部
var contentHeight = document.getElementById("content");
contentHeight.scrollTop = contentHeight.scrollHeight;
});
// return;
},
fail: (res) => {
this.info.pop(obj2);
uni.showToast({
title: res.data.msg, // 提示的内容
icon: 'none', // 提示的图标,有效值: "success", "loading", "none"
duration: 3000 // 提示的持续时间,单位毫秒
});
let obj = {
type: "leftinfo",
time: this.getTodayTime(),
name: "robot",
content: '服务器开小差了,请稍后再试',
question: [],
};
this.info.push(obj);
this.Sending = true;
}
})
this.$nextTick(() => { //滚动聊天到底部
var contentHeight = document.getElementById("content");
contentHeight.scrollTop = contentHeight.scrollHeight;
});
},
saveChatRecord(newChatRecord) {
// 获取当前存储的聊天记录列表,如果不存在则初始化为空数组
let chatRecords = uni.getStorageSync(this.getHistoryId);
// 如果不存在,则初始化为空数组
if (!Array.isArray(chatRecords)) {
chatRecords = [];
}
// 将新的聊天记录对象追加到聊天记录列表中
chatRecords.push(newChatRecord);
// 将更新后的聊天记录列表保存回本地存储
uni.setStorageSync(this.getHistoryId, chatRecords);
},
// 结束语
endMsg() {
let happyEnding = {
type: "leftinfo",
time: this.getTodayTime(),
content: "感谢您使用XXXX,祝您生活愉快",
question: [],
};
this.info.push(happyEnding);
this.$nextTick(() => {
var contentHeight = document.getElementById("content");
contentHeight.scrollTop = contentHeight.scrollHeight;
});
},
showTimer() {
this.timer = setTimeout(this.endMsg, 1000*180);
},
getTodayTime() {
// 获取当前时间
var day = new Date();
let seconds = day.getSeconds();
if (seconds < 10) {
seconds = "0" + seconds;
} else {
seconds = seconds;
}
let minutes = day.getMinutes();
if (minutes < 10) {
minutes = "0" + minutes;
} else {
minutes = minutes;
}
let time =
day.getFullYear() +
"-" +
(day.getMonth() + 1) +
"-" +
day.getDate() +
" " +
day.getHours() +
":" +
minutes +
":" +
seconds;
return time;
},
GetRequest() {
var url = location.search; //获取url中"?"符后的字串
console.log(url);
var theRequest = new Object();
if(url.indexOf("?") != -1)
{
var str = url.substr(1); // 去掉"?"前缀
var params = str.split("&"); // 按照"&"分割成数组
for (var i = 0; i < params.length; i++) {
var keyValue = params[i].split("="); // 按照"="分割成键和值
var key = keyValue[0]; // 键
var value = keyValue[1]; // 值
theRequest[key] = decodeURIComponent(value); // 将键和值存入对象
}
}
console.log(theRequest.userId);
// console.log(typeof this.userId); //object
this.userId = String(theRequest.userId);
this.getHistoryId = 'getHistory_' + this.userId;
// console.log(typeof this.userId); //String
// return theRequest["userId"];
}
},
props: {},
destroyed() {},
};
</script>
<style lang="scss">
// html, body {
// height: 100%;
// margin: 0;
// padding: 0;
// }
body {
-webkit-user-select: text; /* 兼容Webkit内核浏览器 */
-moz-user-select: text; /* 兼容Firefox */
-ms-user-select: text; /* 兼容IE */
user-select: auto; /* 允许文本选择 */
}
.main {
width: 100%;
height: 90%;
// background: linear-gradient(
// 180deg,
// rgba(149, 179, 212, 1) 0%,
// rgba(74, 131, 194, 1) 100%
// );
background: #f5f5f5;
overflow: hidden;
// .contentoy {
// width: 100%;
// height: 20%;
// // background-size: 100% auto;
// // padding: 0;
// }
.box {
width: 100%;
/* width: 680px; */
height: 97%;
background-color: #fafafa;
position: fixed;
padding-left: 20rpx;
padding-right: 20rpx;
// padding: 1.25rem;
#content {
// height: calc(100% - 50rpx);
height: 88%;
overflow-y: scroll;
font-size: 16px;
width: 100%;
-webkit-user-select: auto;
// box-sizing: border-box; /* 确保 padding 和 border 在内容区内 */
.circle {
display: inline-block;
width: 40px;
height: 40px;
border-radius: 50%;
}
.con_text {
color: #333;
margin-bottom: 5px;
margin-left: 5px;
word-break: break-all; /* 自动换行 */
overflow-wrap: break-word; /* 避免长单词超出容器 */
user-select: auto; /* 确保对话框文本可选择 */
}
.con_que {
color: #1c88ff;
height: 30px;
line-height: 30px;
cursor: pointer;
}
.info_r {
position: relative;
margin-top: 10px;
.circle_r {
position: absolute;
left: 0%;
}
.pic_r {
border-radius: 2px;
width: 40px;
height: 40px;
margin: 2px;
}
.con_r {
display: inline-block;
/* max-width: 253px; */
max-width: 70%; /* 限制对话框最大宽度 */
min-width: 30px; /* 限制对话框最小宽度 */
min-height: 40px;
/* min-height: 20px; */
background-color: #e2e2e2;
border-radius: 6px;
padding: 10px;
margin-left: 50px;
}
.time_r {
margin-left: 50px;
color: #999999;
font-size: 12px;
}
}
.info_l {
text-align: right;
color: #ffffff;
color: #3163C5;
margin-top: 10px;
.pic_l {
width: 13px;
height: 17px;
margin: 8px 10px;
}
.time_l {
// margin-right: 45px;
color: #999999;
font-size: 12px;
margin-top: 5px;
}
.con_l {
display: inline-block;
max-width: 70%; /* 限制对话框最大宽度 */
min-width: 30px; /* 限制对话框最小宽度 */
background-color: #1FC59F;
border-radius: 6px;
padding: 10px;
text-align: left;
color: #fff;
margin-right: 5px;
word-break: break-word; /* 确保长单词换行 */
}
}
#question {
cursor: pointer;
}
}
}
}
.setproblem {
width: 100%;
height: 145rpx;
// height: 10%;
background-color: #ffffff;
position: fixed;
bottom: 10rpx;
left: 0;
padding: 20rpx;
box-sizing: border-box;
}
.setproblem textarea {
color: #000000;
width: calc(100% - 7rem); /* 让textarea占据除按钮外的剩余空间 */
height: 100%;
padding: 10px;
box-sizing: border-box;
border: 1px solid #ccc; /* 添加边框 */
border-radius: 5px; /* 添加圆角 */
resize: none; /* 禁止调整大小 */
-webkit-user-select: auto; /* 允许选择文本 */
}
.setproblem button {
width: 5.875rem;
height: 2.5rem;
line-height: 2.5rem;
background: #1FC59F;
opacity: 1;
border-radius: 4px;
font-size: 10px;
color: #ffffff;
position: absolute;
right: 30rpx;
bottom: 50%; /* 让按钮垂直居中 */
transform: translateY(50%); /* 垂直居中的兼容性写法 */
cursor: pointer;
border: none;
}
.czkj-item-title {
line-height: 25px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
margin-bottom: 5px;
}
.czkj-item-question {
cursor: pointer;
display: block;
padding: 8px;
position: relative;
border-bottom: 1px dashed #ccc;
line-height: 20px;
min-height: 20px;
overflow: hidden;
}
.czkj-question-msg {
float: left;
font-size: 14px;
color: #3163C5;
}
</style>
文中有设置页面可复制的内容,但是失败了,如果知道我设置的哪里有问题请一定给我留言,万分感谢。不需要这个功能的也可以直接删掉那部分尝试。