保存今天的修改
This commit is contained in:
+96
-21
@@ -2,6 +2,7 @@ var curSid = 0;
|
||||
var sessions = {};
|
||||
var drafts = {};
|
||||
var queueItems = [];
|
||||
var queueOverdueNotified = new Set();
|
||||
var accepting = new Set();
|
||||
var sendingSid = "";
|
||||
var closingSid = "";
|
||||
@@ -10,6 +11,10 @@ var listSignature = "";
|
||||
var typeTimer = null;
|
||||
var toolsOpen = false;
|
||||
var toolsOpener = null;
|
||||
var staffExpired = false;
|
||||
var staffLoggingOut = false;
|
||||
var staffPollTimers = [];
|
||||
var staffCsrf = document.querySelector('meta[name="staff-csrf-token"]').content;
|
||||
var replyInput = document.getElementById("replyText");
|
||||
SupportUI.initAttachments(document.getElementById('chatBox'), '图片暂时无法访问,请客户重新发送附件。');
|
||||
|
||||
@@ -20,13 +25,66 @@ function escapeText(value){
|
||||
}
|
||||
|
||||
async function adminRequest(path, options){
|
||||
const response = await fetch(path, options);
|
||||
if(!response.ok) throw new Error("HTTP " + response.status);
|
||||
const data = await response.json();
|
||||
if(data && data.ok === false) throw new Error("Request rejected");
|
||||
return data;
|
||||
if(staffExpired) throw new Error("Staff login expired");
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(function(){ controller.abort(); }, 10000);
|
||||
try{
|
||||
const headers = Object.assign({}, options && options.headers);
|
||||
if(options && options.method && options.method !== "GET") headers["X-CSRF-Token"] = staffCsrf;
|
||||
const response = await fetch(path, Object.assign({}, options, {headers:headers, signal:controller.signal}));
|
||||
if(response.status === 401){
|
||||
if(!staffExpired){
|
||||
staffExpired = true;
|
||||
staffPollTimers.forEach(clearInterval);
|
||||
window.location.replace("/admin?expired=1");
|
||||
}
|
||||
throw new Error("Staff login expired");
|
||||
}
|
||||
if(staffExpired) throw new Error("Staff login expired");
|
||||
const data = await response.json();
|
||||
if(!response.ok || (data && data.ok === false)){
|
||||
const error = new Error("HTTP " + response.status);
|
||||
error.status = response.status;
|
||||
error.userMessage = data && (data.e || data.error);
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}finally{
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function logoutStaff(){
|
||||
if(staffLoggingOut || staffExpired) return;
|
||||
staffLoggingOut = true;
|
||||
const button = document.getElementById("staff-logout");
|
||||
const status = document.getElementById("staff-logout-status");
|
||||
button.disabled = true;
|
||||
button.textContent = "正在退出…";
|
||||
status.hidden = true;
|
||||
try{
|
||||
await postAdmin("/admin/logout", {});
|
||||
staffExpired = true;
|
||||
staffPollTimers.forEach(clearInterval);
|
||||
window.location.replace("/admin");
|
||||
}catch(error){
|
||||
if(!staffExpired){
|
||||
status.textContent = "退出失败,请检查网络后重试。";
|
||||
status.hidden = false;
|
||||
}
|
||||
}finally{
|
||||
staffLoggingOut = false;
|
||||
button.disabled = false;
|
||||
button.textContent = "退出登录";
|
||||
}
|
||||
}
|
||||
document.getElementById("staff-logout").onclick = logoutStaff;
|
||||
window.addEventListener("pageshow", function(event){
|
||||
if(event.persisted){
|
||||
adminRequest("/api/admin/session").then(function(data){ staffCsrf = data.csrf_token; }).catch(function(){});
|
||||
}
|
||||
});
|
||||
|
||||
function postAdmin(path, data){
|
||||
return adminRequest(path, {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(data)});
|
||||
}
|
||||
@@ -86,11 +144,16 @@ function renderQueue(){
|
||||
return ("#" + item.id + " " + item.ctime).toLowerCase().includes(term);
|
||||
});
|
||||
document.getElementById("queue-count").textContent = queueItems.length;
|
||||
document.getElementById("queue-overdue-count").textContent = "超时 " + queueItems.filter(function(item){ return item.overdue; }).length;
|
||||
document.getElementById("session-browser").classList.toggle("queue-empty", queueItems.length === 0);
|
||||
document.getElementById("queue").innerHTML = visible.length
|
||||
? visible.map(function(item){
|
||||
const sid = Number(item.id);
|
||||
return '<div class="queue-item"><div class="qt">对话 #'+sid+'<small>'+escapeText(item.ctime)+'</small></div><button type="button" onclick="accept('+sid+')">接入会话 →</button></div>';
|
||||
const age = Number(item.waiting_minutes) || 0;
|
||||
const wait = item.overdue
|
||||
? '<span class="queue-overdue">排队超时 '+escapeText(item.overdue_minutes)+' 分钟</span>'
|
||||
: '<span class="queue-wait">已等待 '+escapeText(age)+' 分钟</span>';
|
||||
return '<div class="queue-item'+(item.overdue ? ' is-overdue' : '')+'"><div class="qt">对话 #'+sid+wait+'<small>'+escapeText(item.ctime)+'</small></div><button type="button" onclick="accept('+sid+')">接入会话 →</button></div>';
|
||||
}).join("")
|
||||
: '<div class="list-empty">'+(term ? "没有匹配的排队会话" : "当前没有排队会话")+'</div>';
|
||||
}
|
||||
@@ -98,6 +161,17 @@ function renderQueue(){
|
||||
async function loadQueue(){
|
||||
try{
|
||||
const list = await adminRequest("/api/human/queue");
|
||||
const waitingOverdue = new Set(list.filter(function(item){ return item.overdue; }).map(function(item){ return String(item.id); }));
|
||||
queueOverdueNotified.forEach(function(id){ if(!waitingOverdue.has(id)) queueOverdueNotified.delete(id); });
|
||||
const newlyOverdue = list.filter(function(item){
|
||||
const id = String(item.id);
|
||||
if(!item.overdue || queueOverdueNotified.has(id)) return false;
|
||||
queueOverdueNotified.add(id);
|
||||
return true;
|
||||
});
|
||||
if(newlyOverdue.length){
|
||||
SupportUI.notify("人工排队超时", newlyOverdue.length+" 位客户已等待超过 5 分钟,请尽快接入。 ");
|
||||
}
|
||||
if(list.some(function(item){ return !queueItems.some(function(old){ return old.id === item.id; }); })){
|
||||
SupportUI.notify("新客户排队", "有 " + list.length + " 位客户等待接入");
|
||||
}
|
||||
@@ -109,15 +183,7 @@ async function loadQueue(){
|
||||
}
|
||||
|
||||
async function loadTickets(){
|
||||
const element = document.getElementById("ticketList");
|
||||
try{
|
||||
const list = await adminRequest("/api/tickets");
|
||||
element.innerHTML = list.length ? list.map(function(ticket){
|
||||
return '<div class="ticket-item">'+escapeText(ticket.content)+'<span>'+escapeText(ticket.ctime)+'</span></div>';
|
||||
}).join("") : '<div class="list-empty">暂时没有客户留言</div>';
|
||||
}catch(error){
|
||||
element.innerHTML = '<div class="list-empty">留言暂时无法加载</div>';
|
||||
}
|
||||
return Promise.all([ticketDesk.load(), ticketDesk.refreshSummary()]);
|
||||
}
|
||||
|
||||
async function accept(sid){
|
||||
@@ -299,7 +365,9 @@ async function sendReply(){
|
||||
if(curSid === sid){ replyInput.focus(); setReplyStatus("已发送"); }
|
||||
poll();
|
||||
}catch(error){
|
||||
if(curSid === sid) setReplyStatus("发送未成功,内容已保留。请确认后重试。", true);
|
||||
if(curSid === sid) setReplyStatus(error.name === 'AbortError'
|
||||
? "发送超时,内容已保留。请先确认聊天记录,再决定是否重试。"
|
||||
: "发送未成功,内容已保留。请确认后重试。", true);
|
||||
}finally{
|
||||
sendingSid = "";
|
||||
updateComposer();
|
||||
@@ -386,14 +454,14 @@ document.getElementById("tools-from-list").onclick = function(){ toolsOpener = "
|
||||
document.getElementById("tools-close").onclick = function(){
|
||||
setToolsOpen(false);
|
||||
const fromList = toolsOpener === "tools-from-list" && !document.body.classList.contains("chat-view") && window.matchMedia("(max-width: 720px)").matches;
|
||||
document.getElementById(fromList ? "tools-from-list" : "tools-toggle").focus();
|
||||
document.getElementById(["ticket-inbox-toggle", "ticket-overdue-toggle"].includes(toolsOpener) ? toolsOpener : fromList ? "tools-from-list" : "tools-toggle").focus();
|
||||
};
|
||||
document.getElementById("tools-backdrop").onclick = document.getElementById("tools-close").onclick;
|
||||
document.addEventListener("keydown", function(event){
|
||||
if(event.key === "Escape" && toolsOpen) document.getElementById("tools-close").click();
|
||||
if(event.key === "Escape") document.getElementById("data-overview").open = false;
|
||||
if(event.key === "Tab" && toolsOpen && window.matchMedia("(max-width: 1200px)").matches){
|
||||
const items = Array.from(document.getElementById("service-tools").querySelectorAll('button:not([disabled]), a[href], textarea:not([disabled])'));
|
||||
const items = Array.from(document.getElementById("service-tools").querySelectorAll('button:not([disabled]):not([hidden]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])')).filter(function(item){ return item.getClientRects().length; });
|
||||
const first = items[0], last = items[items.length-1];
|
||||
if(event.shiftKey && document.activeElement === first){ event.preventDefault(); last.focus(); }
|
||||
else if(!event.shiftKey && document.activeElement === last){ event.preventDefault(); first.focus(); }
|
||||
@@ -414,8 +482,15 @@ setToolsOpen(false);
|
||||
setSessionTab("active");
|
||||
updateComposer();
|
||||
loadQueue();
|
||||
var ticketDesk = window.AdminTickets.create({request:adminRequest, escape:escapeText,
|
||||
username:document.querySelector('meta[name="staff-username"]').content,
|
||||
notify:function(title, body){ SupportUI.notify(title, body); },
|
||||
open:function(opener){
|
||||
toolsOpener = opener || "ticket-inbox-toggle";
|
||||
setToolsOpen(true);
|
||||
document.getElementById("tickets-title").scrollIntoView({block:"start"});
|
||||
document.getElementById("ticket-search").focus({preventScroll:true});
|
||||
}});
|
||||
loadTickets();
|
||||
poll();
|
||||
setInterval(loadQueue, 3000);
|
||||
setInterval(loadTickets, 5000);
|
||||
setInterval(poll, 1000);
|
||||
staffPollTimers = [setInterval(loadQueue, 3000), setInterval(loadTickets, 5000), setInterval(poll, 1000)];
|
||||
|
||||
Reference in New Issue
Block a user