ai-css/static/templates/chat_page.html
2026-03-04 13:34:13 +00:00

772 lines
32 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;" name="viewport" />
<meta name="keywords" content="Golang Open Source LiveChat Software"/>
<meta name="description" content="Golang Open Source LiveChat Software"/>
<title>Live Chat Support</title>
<!-- 防止闪烁:在页面渲染前应用主题 -->
<script>
(function() {
// 1. 从 URL 参数获取主题
const urlParams = new URLSearchParams(window.location.search);
const themeParam = urlParams.get('theme');
// 2. 从 localStorage 获取保存的主题
const savedTheme = localStorage.getItem('theme');
// 3. 检测是否在 iframe 中
const isInIframe = window.parent !== window;
// 4. 确定要使用的主题
// 如果在 iframe 中且没有明确指定主题,默认使用暗色模式(避免闪烁)
let theme;
if (themeParam) {
theme = themeParam;
} else if (savedTheme) {
theme = savedTheme;
} else if (isInIframe) {
// iframe 环境默认暗色,等待父页面发送实际主题
theme = 'dark';
} else {
theme = 'light';
}
// 5. 立即设置HTML背景色和应用主题类
if (theme === 'dark') {
// 暗色模式:立即设置内联样式防止闪烁
document.documentElement.style.backgroundColor = '#1a3d35';
document.documentElement.style.color = '#FFFFFF';
document.documentElement.classList.add('dark-mode-init');
} else {
// 白天模式:移除可能存在的暗色类和样式
document.documentElement.classList.remove('dark-mode-init');
document.documentElement.style.backgroundColor = '';
document.documentElement.style.color = '';
}
// 6. 在脚本末尾立即尝试给body添加类此时body可能还不存在
// 使用定时器快速重试,确保尽早添加
let retryCount = 0;
const maxRetries = 50; // 最多重试50次
const applyBodyClass = function() {
if (document.body) {
if (theme === 'dark') {
document.body.classList.add('dark-mode');
// 移除html的内联样式让CSS接管
document.documentElement.style.backgroundColor = '';
document.documentElement.style.color = '';
} else {
// 白天模式:确保移除暗色类
document.body.classList.remove('dark-mode');
}
return true;
} else if (retryCount < maxRetries) {
retryCount++;
setTimeout(applyBodyClass, 10); // 10ms后重试
return false;
}
return false;
};
applyBodyClass();
})();
</script>
<link rel="stylesheet" href="/aicss/static/cdn/element-ui/2.15.1/theme-chalk/index.min.css">
<script src="/aicss/static/cdn/vue/2.6.11/vue.min.js"></script>
<script src="/aicss/static/cdn/element-ui/2.15.1/index.js"></script>
<script src="/aicss/static/cdn/jquery/3.6.0/jquery.min.js"></script>
<script src="/aicss/assets/js/functions.js?v=fgffdwersdccvcbv"></script>
<script src="/aicss/assets/js/reconnecting-websocket.min.js"></script>
<link rel="stylesheet" href="/aicss/static/css/common.css?v=sdsderfrgfgdfdf" />
<link rel="stylesheet" href="/aicss/static/css/icono.min.css" />
<link rel="stylesheet" href="/aicss/static/css/icon/iconfont.css?v=fgjlgfda"/>
<link rel="stylesheet" href="/aicss/static/css/dark-mode.css" />
</head>
<body class="visitorBody">
<div id="app" class="chatCenter">
<template>
<!--Customer Service Code-->
<div class="chatEntTitle" v-show="!isIframe">
<el-badge type="success" is-dot class="item">
<el-avatar class="chatEntTitleLogo" :size="35" :src="kefuInfo.avatar"></el-avatar>
</el-badge>
<div>
<div>在线客服</div>
</div>
</div>
<div class="chatEntBox">
<div class="chatContext chatVisitorPage">
<div class="chatBox">
<div class="chatNotice" v-on:click="getHistoryMessage" v-show="showLoadMore">
<a href="javascript:;" class="chatNoticeContent" style="color: #07a9fe;">加载更多消息</a>
</div>
<el-row :gutter="2" v-for="v in msgList" v-bind:class="{'chatBoxMe': v.is_kefu==false}">
<div class="chatTime" v-bind:class="{'chatTimeHide': v.show_time==false}"><span><{v.time}></span></div>
<div v-if="v.is_kefu==true" style="display: flex;">
<el-avatar style="margin-right:10px;flex-shrink: 0;" :size="36" :src="v.avator"></el-avatar>
<div class="chatMsgContent">
<div class="chatContent chatContent2 replyContentBtn" v-html="v.content"></div>
</div>
</div>
<div class="kefuMe" v-if="v.is_kefu==false" style="display: flex;justify-content: flex-end;">
<div class="chatContent chatContent2 replyContentBtn msg-bubble-wrap" :class="{'msg-bubble-failed': v.status=='failed'}">
<span v-html="v.content"></span>
<span class="msg-status-icon msg-status-sending" v-if="v.status=='sending'"><i class="el-icon-loading"></i></span>
<span class="msg-status-icon msg-status-failed" v-if="v.status=='failed'" @click.stop="resendMessage(v)" title="发送失败,点击重试"><i class="el-icon-warning"></i></span>
</div>
<el-avatar style="margin-left:10px;flex-shrink: 0;" :size="36" :src="v.avator"></el-avatar>
</div>
<div class="clear"></div>
</el-row>
</div>
</div>
<div class="chatBoxSend">
<div class="visitorIconBox">
<el-tooltip content="Send emoji" placement="top">
<div class="iconBtn iconfont icon-xiaolian" style="margin-left:15px;font-size: 20px;cursor: pointer;" @click.stop="showFaceIcon==true?showFaceIcon=false:showFaceIcon=true"></div>
</el-tooltip>
<el-tooltip content="Upload image" placement="top">
<div class="iconBtn el-icon-picture" id="uploadImg" v-on:click="uploadImg('/uploadimg')" style="font-size: 20px;"></div>
</el-tooltip>
<el-tooltip content="Upload file" placement="top">
<div class="iconBtn el-icon-upload" id="uploadFile" v-on:click="uploadFile('/uploadfile')" style="font-size: 20px;"></div>
</el-tooltip>
<div style="display: none" title="Emoji" class="icono-smile visitorIconBtns visitorFaceBtn"></div>
<div class="clear"></div>
</div>
<div class="faceBox visitorFaceBox" v-show="showFaceIcon">
<ul class="faceBoxList">
<li v-on:click="faceIconClick(i)" class="faceIcon" v-for="(v,i) in face" :title="v.name"><img :src=v.path></li>
</ul>
<div class="clear"></div>
</div>
<div class="message-box-send">
<el-input :rows="2" type="textarea" resize="none" class="visitorEditorArea" @focus="scrollBottom;showIconBtns=false" @blur="scrollBottom;showIconBtns=false" v-model="messageContent" v-on:keyup.enter.native="chatToUser">
</el-input>
<div class="btn-box">
<el-button type="primary" size="mini" class="visitorEditorBtn" :disabled="messageContent==''" v-on:click="chatToUser();showIconBtns=false">发送</el-button>
</div>
</div>
</div>
</div>
<div class="chatArticle">
<h3 class="hotQuestionTitle">公告</h3>
<div class="allNotice" v-html><{kefuInfo.allNotice}></div>
</div>
<audio id="chatMessageAudio"></audio>
<audio id="chatMessageSendAudio"></audio>
</template>
</div>
</body>
<script>
var KEFU_ID='{{.KEFU_ID}}';
var REFER='{{.Refer}}';
// 主题切换函数
function setTheme(theme) {
if (theme === 'dark') {
document.body.classList.add('dark-mode');
localStorage.setItem('theme', 'dark');
} else {
document.body.classList.remove('dark-mode');
localStorage.setItem('theme', 'light');
}
}
// 监听来自父页面的主题变化消息
window.addEventListener('message', function(event) {
// 安全检查:可以添加 origin 验证
// if (event.origin !== 'https://your-parent-domain.com') return;
if (event.data && event.data.type === 'theme-change') {
console.log('Received theme change:', event.data.theme);
setTheme(event.data.theme);
}
});
// 页面加载时,应用主题(已在 head 中预设)
(function() {
// 检查是否在 head 中已经设置了暗色模式
const isDarkInit = document.documentElement.classList.contains('dark-mode-init');
if (isDarkInit) {
// 已在 head 中设置为暗色,直接应用
document.body.classList.add('dark-mode');
localStorage.setItem('theme', 'dark');
// 移除临时类
document.documentElement.classList.remove('dark-mode-init');
} else {
// 确保是亮色模式
document.body.classList.remove('dark-mode');
localStorage.setItem('theme', 'light');
}
// 通知父页面已准备好接收主题消息
if (window.parent !== window) {
window.parent.postMessage({ type: 'iframe-ready' }, '*');
}
})();
</script>
<script>
var initVue=function(){
new Vue({
el: '#app',
delimiters:["<{","}>"],
data: {
window:window,
server:getWsBaseUrl()+"/aicss/ws_visitor",
socket:null,
msgList:[],
msgListNum:[],
messageContent:"",
chatTitle:"Connecting...",
visitor:{},
face:[],
showKfonline:false,
socketClosed:false,
focusSendConn:false,
timer:null,
sendDisabled:false,
showIconBtns:false,
showFaceIcon:false,
isIframe:false,
kefuInfo:{},
showLoadMore:false,
messages:{
page:1,
pagesize:5,
list:[],
},
},
methods: {
initConn:function() {
let socket = new ReconnectingWebSocket(this.server+"?visitor_id="+this.visitor.visitor_id);
this.socket = socket
this.socket.onmessage = this.OnMessage;
this.socket.onopen = this.OnOpen;
this.socket.onclose = this.OnClose;
this.ping();
},
OnOpen:function() {
console.log("ws:onopen");
this.getNotice();
this.socketClosed=false;
this.focusSendConn=false;
},
OnMessage:function(e) {
console.log("ws:onmessage");
this.socketClosed=false;
this.focusSendConn=false;
const redata = JSON.parse(e.data);
if (redata.type == "kfOnline") {
let msg = redata.data
if(this.showKfonline && this.visitor.to_id==msg.id){
return;
}
this.visitor.to_id=msg.id;
this.showTitle(msg.name+", 正正与您对话");
this.scrollBottom();
this.showKfonline=true;
}
if (redata.type == "transfer") {
var kefuId = redata.data
if(!kefuId){
return;
}
this.visitor.to_id=kefuId;
}
if (redata.type == "notice") {
let msg = redata.data
if(!msg){
return;
}
this.chatTitle=msg
$(".chatBox").append("<div class=\"chatTime\">"+this.chatTitle+"</div>");
this.scrollBottom();
}
if (redata.type == "message") {
let msg = redata.data
this.visitor.to_id=msg.id;
let content = {}
content.avator = msg.avator;
content.name = msg.name;
content.content =replaceContent(msg.content);
content.is_kefu = true;
content.time = msg.time;
this.msgList.push(content);
notify(msg.name, {
body: msg.content,
icon: msg.avator
},function(notification) {
window.focus();
notification.close();
});
this.scrollBottom();
flashTitle();
clearInterval(this.timer);
this.alertSound();
}
if (redata.type == "close") {
this.chatTitle="The conversation has ended";
$(".chatBox").append("<div class=\"chatTime\">"+this.chatTitle+"</div>");
this.scrollBottom();
this.socket.close();
this.focusSendConn=true;
}
if (redata.type == "force_close") {
this.chatTitle="The conversation was terminated by the agent";
$(".chatBox").append("<div class=\"chatTime\">"+this.chatTitle+"</div>");
this.scrollBottom();
this.socket.close();
this.socketClosed=true;
}
if (redata.type == "auto_close") {
this.chatTitle="The conversation timed out due to inactivity";
$(".chatBox").append("<div class=\"chatTime\">"+this.chatTitle+"</div>");
this.scrollBottom();
this.socket.close();
this.socketClosed=true;
}
window.parent.postMessage(redata,"*");
},
chatToUser:function() {
var messageContent=this.messageContent.trim("\r\n");
messageContent=messageContent.replace("\n","");
messageContent=messageContent.replace("\r\n","");
if(messageContent==""||messageContent=="\r\n"){
this.messageContent="";
return;
}
this.messageContent=messageContent;
if(this.socketClosed){
this.$message({
message: 'Connection closed! Please refresh the page',
type: 'warning'
});
return;
}
let _this=this;
let content = {}
content.avator=_this.visitor.avator;
content.content = replaceContent(_this.messageContent);
content.name = _this.visitor.name;
content.is_kefu = false;
content.time = _this.getNowDate();
content.show_time=false;
content.status = 'sending';
_this.msgList.push(content);
_this.scrollBottom();
let mes = {};
mes.type = "visitor";
mes.content = this.messageContent;
mes.from_id = this.visitor.visitor_id;
mes.to_id = this.visitor.to_id;
mes.content = this.messageContent;
content._rawMsg = Object.assign({}, mes);
this.messageContent = ""
$.post("/aicss/2/message",mes,function(res){
if(res.code!=200){
_this.$set(content, 'status', 'failed');
return;
}
_this.$set(content, 'status', 'sent');
clearInterval(_this.timer);
_this.sendSound();
}).fail(function(){
_this.$set(content, 'status', 'failed');
});
},
resendMessage:function(msg) {
if(msg.status !== 'failed') return;
let _this = this;
_this.$set(msg, 'status', 'sending');
let mes = msg._rawMsg;
$.post("/aicss/2/message", mes, function(res){
if(res.code!=200){
_this.$set(msg, 'status', 'failed');
return;
}
_this.$set(msg, 'status', 'sent');
_this.sendSound();
}).fail(function(){
_this.$set(msg, 'status', 'failed');
});
},
OnClose:function() {
console.log("ws:onclose");
this.focusSendConn=true;
},
getUserInfo:function(){
let obj=this.getCache("visitor_"+KEFU_ID);
var visitor_id=""
var to_id=KEFU_ID;
if(obj){
visitor_id=obj.visitor_id;
}
let _this=this;
var extra=getQuery("extra");
$.post("/aicss/visitor_login",{visitor_id:visitor_id,refer:REFER,to_id:to_id,extra:extra},function(res){
if(res.code!=200){
_this.$message({
message: res.msg,
type: 'error'
});
_this.sendDisabled=true;
return;
}
_this.visitor=res.result;
_this.getHistoryMessage();
_this.setCache("visitor_"+KEFU_ID,res.result);
_this.initConn();
});
},
getHistoryMessage:function(){
let params={
page:this.messages.page,
pagesize: this.messages.pagesize,
visitor_id: this.visitor.visitor_id,
}
let _this=this;
$.get("/aicss/2/messagesPages",params,function(res){
let msgList=res.result.list;
if(msgList.length>=_this.messages.pagesize){
_this.showLoadMore=true;
}else{
_this.showLoadMore=false;
}
for(let i in msgList){
let item = msgList[i];
let content = {}
if (item["mes_type"] == "kefu") {
item.is_kefu = true;
item.avator=item["kefu_avator"];
} else {
item.is_kefu = false;
item.avator=item["visitor_avator"];
}
item.time = item["create_time"];
item.content=replaceContent(item["content"]);
_this.msgList.unshift(item);
}
if(_this.messages.page==1){
_this.scrollBottom();
}
_this.messages.page++;
});
},
scrollBottom:function(){
var _this=this;
this.$nextTick(function(){
$('.chatVisitorPage').scrollTop($(".chatVisitorPage")[0].scrollHeight);
});
},
getNowDate : function() {
var d = new Date(new Date());
return d.getFullYear() + '-' + this.digit(d.getMonth() + 1) + '-' + this.digit(d.getDate())
+ ' ' + this.digit(d.getHours()) + ':' + this.digit(d.getMinutes()) + ':' + this.digit(d.getSeconds());
},
digit : function (num) {
return num < 10 ? '0' + (num | 0) : num;
},
setCache : function (key,obj){
if(navigator.cookieEnabled&&typeof window.localStorage !== 'undefined'){
localStorage.setItem(key, JSON.stringify(obj));
}
},
getCache : function (key){
if(navigator.cookieEnabled&&typeof window.localStorage !== 'undefined') {
return JSON.parse(localStorage.getItem(key));
}
},
getNotice : function (){
let _this=this;
$.get("/aicss/notice?kefu_id="+KEFU_ID,function(res) {
var code=res.code;
if(code!=200) return;
_this.kefuInfo=res.result;
_this.showTitle(_this.kefuInfo.nickname+" 为您服务.");
if(!_this.kefuInfo.welcome) return;
var msg={
content:replaceContent(_this.kefuInfo.welcome),
avator:_this.kefuInfo.avatar,
name :_this.kefuInfo.nickname,
time:_this.getNowDate(),
is_kefu:true,
}
_this.msgList.push(msg);
_this.scrollBottom();
_this.alertSound();
});
},
initCss:function(){
var _this=this;
$(function () {
var faces=placeFace();
$.each(faceTitles, function (index, item) {
_this.face.push({"name":item,"path":faces[item]});
});
var windheight = $(window).height();
$(window).resize(function(){
var docheight = $(window).height();
$('body').scrollTop(99999999);
});
});
},
ping:function(){
let _this=this;
let mes = {}
mes.type = "ping";
mes.data = "visitor:"+_this.visitor.visitor_id;
setInterval(function () {
if(_this.socket!=null){
_this.socket.send(JSON.stringify(mes));
}
},10000);
},
init:function(){
var _this=this;
this.initCss();
$('body').click(function(){
clearFlashTitle();
window.parent.postMessage({type:"focus"},"*");
$('.faceBox').hide();
});
window.onfocus = function () {
clearFlashTitle();
window.parent.postMessage({type:"focus"},"*");
if(_this.socketClosed){
return;
}
if(!_this.focusSendConn){
return;
}
_this.initConn();
_this.scrollBottom();
}
},
faceIconClick:function(index){
$('.faceBox').hide();
this.messageContent+="face"+this.face[index].name;
},
uploadImg:function (url){
let _this=this;
$('#uploadImg').after('<input type="file" accept="image/gif,image/jpeg,image/jpg,image/png" id="uploadImgFile" name="file" style="display:none" >');
$("#uploadImgFile").click();
$("#uploadImgFile").change(function (e) {
var formData = new FormData();
var file = $("#uploadImgFile")[0].files[0];
formData.append("imgfile",file);
filter(file) && $.ajax({
url: url || '',
type: "post",
data: formData,
contentType: false,
processData: false,
dataType: 'JSON',
mimeType: "multipart/form-data",
success: function (res) {
if(res.code!=200){
_this.$message({
message: res.msg,
type: 'error'
});
}else{
_this.messageContent+='img[/' + res.result.path + ']';
_this.chatToUser();
}
},
error: function (data) {
console.log(data);
}
});
});
},
uploadFile:function (url){
let _this=this;
$('#uploadFile').after('<input type="file" id="uploadRealFile" name="file2" style="display:none" >');
$("#uploadRealFile").click();
$("#uploadRealFile").change(function (e) {
var formData = new FormData();
var file = $("#uploadRealFile")[0].files[0];
formData.append("realfile",file);
console.log(formData);
$.ajax({
url: url || '',
type: "post",
data: formData,
contentType: false,
processData: false,
dataType: 'JSON',
mimeType: "multipart/form-data",
success: function (res) {
if(res.code!=200){
_this.$message({
message: res.msg,
type: 'error'
});
}else{
var data=JSON.stringify({
name:res.result.name,
ext:res.result.ext,
size:res.result.size,
path:'/' + res.result.path,
})
_this.messageContent+='attachment['+data+']';
_this.chatToUser();
}
},
error: function (data) {
console.log(data);
}
});
});
},
onPasteUpload:function(event){
let items = event.clipboardData && event.clipboardData.items;
let file = null
if (items && items.length) {
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
}
}
}
if (!file) {
return;
}
let _this=this;
var formData = new FormData();
formData.append('imgfile', file);
$.ajax({
url: '/aicss/uploadimg',
type: "post",
data: formData,
contentType: false,
processData: false,
dataType: 'JSON',
mimeType: "multipart/form-data",
success: function (res) {
if(res.code!=200){
_this.$message({
message: res.msg,
type: 'error'
});
}else{
_this.messageContent+='img[/' + res.result.path + ']';
_this.chatToUser();
}
},
error: function (data) {
console.log(data);
}
});
},
alertSound:function(){
alertSound("chatMessageAudio",'/aicss/static/images/alert2.ogg');
},
sendSound:function(){
alertSound("chatMessageSendAudio",'/aicss/static/images/sent.ogg');
},
sendAjax:function(url,method,params,callback){
let _this=this;
$.ajax({
type: method,
url: url,
data:params,
error:function(res){
var data=JSON.parse(res.responseText);
console.log(data);
if(data.code!=200){
_this.$message({
message: data.msg,
type: 'error'
});
}
},
success: function(data) {
if(data.code!=200){
_this.$message({
message: data.msg,
type: 'error'
});
}else if(data.result!=null){
callback(data.result);
}else{
callback(data);
}
}
});
},
showTitle:function(title){
$(".chatBox").append("<div class='chatNotice'><div class=\"chatNoticeContent\"><span>"+title+"</span></div></div>");
this.scrollBottom();
},
},
mounted:function() {
document.addEventListener('paste', this.onPasteUpload)
document.addEventListener('scroll',this.textareaBlur)
},
created: function () {
this.init();
this.getUserInfo();
}
})
}
if(KEFU_ID==""){
var visitor_id="";
if(window.localStorage){
for(var i=0;i<localStorage.length;i++){
var key = localStorage.key(i);
if(key.indexOf("visitor_")==0){
var obj = JSON.parse(localStorage.getItem(key));
if(obj && obj.visitor_id){
visitor_id = obj.visitor_id;
break;
}
}
}
}
$.ajax({
type: "get",
url: "/aicss/kefu_idle?visitor_id="+visitor_id,
success: function(data) {
if(data.code==200 && data.msg=="ok"){
KEFU_ID=data.result;
var oldResult = data.oldResult;
if (oldResult && oldResult != KEFU_ID) {
var oldKey = "visitor_" + oldResult;
var newKey = "visitor_" + KEFU_ID;
var oldCache = localStorage.getItem(oldKey);
if(oldCache) {
localStorage.setItem(newKey, oldCache);
localStorage.removeItem(oldKey);
}
}
}else{
KEFU_ID="default";
}
initVue();
},
error: function(){
KEFU_ID="default";
initVue();
}
});
}else{
initVue();
}
</script>
</html>