加会话转接、满意度评价、会话备注、浏览器通知

This commit is contained in:
songxinnuo
2026-10-29 11:24:11 +08:00
parent 761c629788
commit 2d6806b15d
43 changed files with 434 additions and 27 deletions
+164 -4
View File
@@ -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,"&lt;");
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(); }
});