加会话转接、满意度评价、会话备注、浏览器通知
This commit is contained in:
+164
-4
@@ -64,12 +64,25 @@ body{font-family:-apple-system,"Microsoft YaHei",sans-serif;background:#f0f2f5;h
|
||||
<span>今日 <b id="s3">-</b></span>
|
||||
<span>👍 <b id="s4">-</b></span>
|
||||
<span>👎 <b id="s5">-</b></span>
|
||||
<span>⭐ <b id="s6">-</b></span>
|
||||
</div>
|
||||
<div class="status">
|
||||
<span id="nightBadge" style="display:none;font-size:12px;background:#1e3a5f;color:#fbbf24;padding:3px 10px;border-radius:10px;margin-right:12px;">🌙 夜间时段</span>
|
||||
<a href="/api/export_chat" style="font-size:13px;color:#2978e8;text-decoration:none;margin-right:16px;">导出Excel</a>
|
||||
<span class="dot"></span> 在线
|
||||
</div>
|
||||
<div class="status"><a href="/api/export_chat" style="font-size:13px;color:#2978e8;text-decoration:none;margin-right:16px;">导出聊天记录</a><span class="dot"></span> 在线</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="conv-list">
|
||||
<div style="padding:10px;border-bottom:1px solid #f0f0f0;">
|
||||
<input id="searchBox" placeholder="搜索聊天记录..." style="width:100%;padding:6px 8px;border:1px solid #ddd;border-radius:4px;font-size:12px;box-sizing:border-box;" onkeydown="if(event.key==='Enter')doSearch()">
|
||||
</div>
|
||||
<div id="searchResult" style="position:fixed;top:70px;right:20px;background:#fff;border:2px solid #dc2626;border-radius:8px;max-height:500px;overflow-y:auto;z-index:9999;display:none;width:500px;box-shadow:0 4px 16px rgba(0,0,0,0.2);resize:both;overflow:auto;min-width:300px;min-height:200px;">
|
||||
<button onclick="this.parentElement.style.display='none';document.getElementById('searchBox').value='';" style="position:absolute;top:8px;right:12px;border:none;background:#fff;color:#dc2626;border-radius:50%;width:30px;height:30px;font-size:16px;cursor:pointer;z-index:10;font-weight:bold;">✕</button>
|
||||
<div id="searchHeader" style="padding:12px;background:#dc2626;color:#fff;font-weight:bold;cursor:move;">搜索结果</div>
|
||||
<div id="searchBody"></div>
|
||||
</div>
|
||||
<h3>待接入</h3>
|
||||
<div id="queue"></div>
|
||||
<h3>进行中</h3>
|
||||
@@ -81,10 +94,24 @@ body{font-family:-apple-system,"Microsoft YaHei",sans-serif;background:#f0f2f5;h
|
||||
<div id="chatContent" style="display:none;flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
<div class="chat-header">
|
||||
<span class="ct" id="chatTitle">对话</span>
|
||||
<button class="close" onclick="closeSession()">结束会话</button>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button onclick="transferSession()" style="background:#f59e0b;color:#fff;border:none;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:13px;">转接</button>
|
||||
<button class="close" onclick="closeSession()">结束会话</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chatBox"></div>
|
||||
<div class="input-bar">
|
||||
<button onclick="document.getElementById('emojiPanel2').style.display=document.getElementById('emojiPanel2').style.display==='block'?'none':'block'" style="font-size:20px;background:none;border:none;cursor:pointer;">😊</button>
|
||||
<div id="emojiPanel2" style="display:none;position:absolute;bottom:60px;left:10px;background:#fff;border:1px solid #ddd;border-radius:8px;padding:10px;box-shadow:0 2px 8px rgba(0,0,0,0.15);z-index:99;">
|
||||
<span onclick="insertEmoji2('😊')" style="cursor:pointer;font-size:20px;margin:2px;">😊</span>
|
||||
<span onclick="insertEmoji2('😂')" style="cursor:pointer;font-size:20px;margin:2px;">😂</span>
|
||||
<span onclick="insertEmoji2('👍')" style="cursor:pointer;font-size:20px;margin:2px;">👍</span>
|
||||
<span onclick="insertEmoji2('❤️')" style="cursor:pointer;font-size:20px;margin:2px;">❤️</span>
|
||||
<span onclick="insertEmoji2('🙏')" style="cursor:pointer;font-size:20px;margin:2px;">🙏</span>
|
||||
<span onclick="insertEmoji2('😅')" style="cursor:pointer;font-size:20px;margin:2px;">😅</span>
|
||||
<span onclick="insertEmoji2('😢')" style="cursor:pointer;font-size:20px;margin:2px;">😢</span>
|
||||
<span onclick="insertEmoji2('🎉')" style="cursor:pointer;font-size:20px;margin:2px;">🎉</span>
|
||||
</div>
|
||||
<input type="text" id="replyText" placeholder="输入回复,Enter发送">
|
||||
<button onclick="sendReply()">发送</button>
|
||||
</div>
|
||||
@@ -96,6 +123,11 @@ body{font-family:-apple-system,"Microsoft YaHei",sans-serif;background:#f0f2f5;h
|
||||
<h4>客户信息</h4>
|
||||
<div class="info-row" id="cInfo">选择对话后显示</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4>会话备注</h4>
|
||||
<textarea id="noteInput" placeholder="给这个对话加备注..." style="width:100%;height:60px;border:1px solid #ddd;border-radius:4px;padding:6px;font-size:12px;resize:none;box-sizing:border-box;"></textarea>
|
||||
<button onclick="saveNote()" style="width:100%;margin-top:6px;background:#2978e8;color:#fff;border:none;padding:5px;border-radius:4px;cursor:pointer;font-size:12px;">保存备注</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4>快捷回复</h4>
|
||||
<div class="quick" onclick="quickReply('您好,请问有什么可以帮您?')">您好,请问有什么可以帮您?</div>
|
||||
@@ -114,17 +146,86 @@ body{font-family:-apple-system,"Microsoft YaHei",sans-serif;background:#f0f2f5;h
|
||||
var curSid = 0;
|
||||
var sessions = {};
|
||||
|
||||
// 浏览器通知权限
|
||||
if("Notification" in window && Notification.permission === "default"){
|
||||
Notification.requestPermission();
|
||||
}
|
||||
function notify(title, body){
|
||||
if(!("Notification" in window) || Notification.permission !== "granted") return;
|
||||
if(!document.hidden) return; // 页面可见时不弹
|
||||
var n = new Notification(title, {body: body, icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎧</text></svg>"});
|
||||
n.onclick = function(){ window.focus(); n.close(); };
|
||||
}
|
||||
|
||||
// 夜间时段提示(22:00-8:00)
|
||||
(function checkNight(){
|
||||
var h = new Date().getHours();
|
||||
var badge = document.getElementById("nightBadge");
|
||||
if(h >= 22 || h < 8){ badge.style.display = "inline-block"; }
|
||||
setInterval(checkNight, 60000);
|
||||
})();
|
||||
|
||||
fetch("/api/admin/stats").then(function(r){return r.json()}).then(function(d){
|
||||
s1.textContent=d.total_conv; s2.textContent=d.total_msg;
|
||||
s3.textContent=d.today_conv; s4.textContent=d.good; s5.textContent=d.bad;
|
||||
s6.textContent=d.rating_avg+"("+d.rating_count+")";
|
||||
});
|
||||
|
||||
// 拖拽搜索弹窗
|
||||
(function(){
|
||||
var header = document.getElementById("searchHeader");
|
||||
var box = document.getElementById("searchResult");
|
||||
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
||||
header.onmousedown = dragMouseDown;
|
||||
function dragMouseDown(e){
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
pos3 = e.clientX;
|
||||
pos4 = e.clientY;
|
||||
document.onmouseup = closeDragElement;
|
||||
document.onmousemove = elementDrag;
|
||||
}
|
||||
function elementDrag(e){
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
pos1 = pos3 - e.clientX;
|
||||
pos2 = pos4 - e.clientY;
|
||||
pos3 = e.clientX;
|
||||
pos4 = e.clientY;
|
||||
box.style.right = "auto";
|
||||
box.style.left = (box.offsetLeft - pos1) + "px";
|
||||
box.style.top = (box.offsetTop - pos2) + "px";
|
||||
}
|
||||
function closeDragElement(){
|
||||
document.onmouseup = null;
|
||||
document.onmousemove = null;
|
||||
}
|
||||
})();
|
||||
|
||||
function doSearch(){
|
||||
var q = document.getElementById("searchBox").value.trim();
|
||||
if(!q) return;
|
||||
fetch("/api/search_chat?q="+encodeURIComponent(q)).then(function(r){return r.json()}).then(function(list){
|
||||
document.getElementById("searchResult").style.display = "block";
|
||||
var body = document.getElementById("searchBody");
|
||||
if(!list.length){ body.innerHTML = '<div style="padding:10px;color:#999;">无结果</div>'; return; }
|
||||
body.innerHTML = list.map(function(r){
|
||||
return '<div style="padding:8px 12px;border-bottom:1px solid #f0f0f0;font-size:12px;"><b>'+r.title+'</b> ['+r.role+']<br>'+r.content+'<br><span style="color:#999;">'+r.ctime+'</span></div>';
|
||||
}).join("");
|
||||
});
|
||||
}
|
||||
|
||||
function loadQueue(){
|
||||
fetch("/api/human/queue").then(function(r){return r.json()}).then(function(list){
|
||||
var prevCount = document.getElementById("queue").dataset.count || 0;
|
||||
var curCount = list.length;
|
||||
document.getElementById("queue").dataset.count = curCount;
|
||||
if(curCount > prevCount && document.hidden){
|
||||
notify("新客户排队", "有 " + curCount + " 位客户等待接入");
|
||||
}
|
||||
document.getElementById("queue").innerHTML = list.length
|
||||
? list.map(function(x){return '<div class="queue-item"><div class="qt">#'+x.id+' '+x.ctime+'</div><button onclick="accept('+x.id+')">接入</button></div>'}).join("")
|
||||
: '<div style="padding:10px 14px;font-size:12px;color:#ccc;">暂无排队</div>';
|
||||
if(list.length && !sessions[String(list[0].id)]) accept(list[0].id);
|
||||
});
|
||||
}
|
||||
setInterval(loadQueue, 3000);
|
||||
@@ -160,22 +261,60 @@ function switchSession(sid){
|
||||
sessions[sid].msgs.forEach(function(m){ appendMsg(m); });
|
||||
sessions[sid].unread = 0;
|
||||
renderList();
|
||||
// 从后端拉备注
|
||||
document.getElementById("noteInput").value = "加载中...";
|
||||
fetch("/api/human/poll?sid="+parseInt(sid)+"&after=0").then(function(r){return r.json()}).then(function(d){
|
||||
document.getElementById("noteInput").value = d.note || "";
|
||||
});
|
||||
fetch("/api/human/read",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:parseInt(sid)})});
|
||||
}
|
||||
|
||||
function saveNote(){
|
||||
if(!curSid){ alert("请先选择一个对话"); return; }
|
||||
var note = document.getElementById("noteInput").value;
|
||||
sessions[curSid].note = note;
|
||||
fetch("/api/human/note",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:parseInt(curSid), note:note})}).then(function(){
|
||||
var btn = event.target;
|
||||
var old = btn.textContent;
|
||||
btn.textContent = "已保存 ✓";
|
||||
btn.style.background = "#22c55e";
|
||||
setTimeout(function(){ btn.textContent = old; btn.style.background = "#2978e8"; }, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function appendMsg(m){
|
||||
var div=document.createElement("div");
|
||||
if(m.side==="system"){
|
||||
div.className="msg system-msg";
|
||||
div.innerHTML='<div style="text-align:center;font-size:12px;color:#999;padding:4px 0;">'+m.content+'</div>';
|
||||
document.getElementById("chatBox").appendChild(div);
|
||||
document.getElementById("chatBox").scrollTop=999999;
|
||||
return;
|
||||
}
|
||||
div.className="msg "+(m.side==="customer"?"cust":"agent");
|
||||
var c = m.content.replace(/</g,"<");
|
||||
var imgMatch = c.match(/\[图片\](\/uploads\/\S+)/);
|
||||
if(imgMatch){
|
||||
c = c.replace(imgMatch[0], '<img src="'+imgMatch[1]+'" style="max-width:200px;border-radius:6px;display:block;">');
|
||||
}
|
||||
div.innerHTML='<div><div class="bubble">'+c+'</div><div class="meta">'+(m.side==="customer"?"客户":"客服")+'</div></div>';
|
||||
var recallBtn = m.side==="agent" ? '<span onclick="recallMsg('+m.id+',this)" style="font-size:11px;color:#999;cursor:pointer;margin-left:6px;">撤回</span>' : '';
|
||||
div.innerHTML='<div><div class="bubble">'+c+'</div><div class="meta">'+(m.side==="customer"?"客户":"客服")+recallBtn+'</div></div>';
|
||||
document.getElementById("chatBox").appendChild(div);
|
||||
document.getElementById("chatBox").scrollTop=999999;
|
||||
}
|
||||
|
||||
function recallMsg(mid, el){
|
||||
fetch("/api/human/recall",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mid:mid})}).then(function(){
|
||||
var msgDiv = el.closest(".msg");
|
||||
if(msgDiv){
|
||||
var bubble = msgDiv.querySelector(".bubble");
|
||||
if(bubble) bubble.innerHTML = "";
|
||||
var meta = msgDiv.querySelector(".meta");
|
||||
if(meta) meta.innerHTML = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderList(){
|
||||
var el=document.getElementById("sessionList");
|
||||
el.innerHTML="";
|
||||
@@ -217,6 +356,9 @@ function poll(){
|
||||
sessions[sid].lastId=m.id;
|
||||
sessions[sid].msgs.push(m);
|
||||
if(m.side==="customer" && sid!=curSid){ sessions[sid].unread=(sessions[sid].unread||0)+1; renderList(); }
|
||||
if(m.side==="customer" && document.hidden){
|
||||
notify("客户消息 #"+sid, m.content.substring(0, 50));
|
||||
}
|
||||
if(sid==curSid) appendMsg(m);
|
||||
});
|
||||
}).catch(function(){});
|
||||
@@ -225,6 +367,12 @@ function poll(){
|
||||
}
|
||||
setInterval(poll, 1000);
|
||||
|
||||
function insertEmoji2(e){
|
||||
document.getElementById("replyText").value += e;
|
||||
document.getElementById("replyText").focus();
|
||||
document.getElementById("emojiPanel2").style.display = "none";
|
||||
}
|
||||
|
||||
function sendReply(){
|
||||
var t=document.getElementById("replyText").value.trim();
|
||||
if(!t || !curSid) return;
|
||||
@@ -247,6 +395,18 @@ function closeSession(){
|
||||
renderList();
|
||||
}
|
||||
|
||||
function transferSession(){
|
||||
if(!curSid) return;
|
||||
if(!confirm("确定转接这个对话?转接后其他客服可在排队列表接入。")) return;
|
||||
fetch("/api/human/transfer",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:parseInt(curSid)})}).then(function(){
|
||||
delete sessions[curSid];
|
||||
curSid=0;
|
||||
document.getElementById("chatContent").style.display="none";
|
||||
document.getElementById("emptyTip").style.display="flex";
|
||||
renderList();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("replyText").addEventListener("keydown", function(e){
|
||||
if(e.key==="Enter"){ e.preventDefault(); sendReply(); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user