Files
baiyue-logistics-chat/static/js/admin_routing.js
T

171 lines
10 KiB
JavaScript
Raw 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.
/* Group configuration, explicit availability and versioned manual reassignment. */
(function(root){
function create(options){
const el = id => document.getElementById(id);
const escape = value => String(value == null ? '' : value).replace(/[&<>"']/g,
character => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[character]));
let snapshot = null, busy = false, beating = false, generation = 0, selectedVersion = null;
const controls = ['routing-presence-toggle','routing-refresh','routing-create-group','routing-save-member','routing-save-assignment'];
function message(text, error){
['routing-status','routing-management-status'].forEach(id=>{
el(id).textContent = text || '';
el(id).classList.toggle('is-error', Boolean(error));
});
}
function setBusy(value){
busy = value;
controls.forEach(id => { el(id).disabled = value || !snapshot; });
}
function fill(id, items, placeholder){
const input = el(id), previous = input.value;
input.innerHTML = (placeholder ? '<option value="">'+escape(placeholder)+'</option>' : '') +
items.map(item => '<option value="'+escape(item.value)+'">'+escape(item.label)+'</option>').join('');
input.value = items.some(item => String(item.value) === previous) ? previous :
(placeholder ? '' : items.length ? String(items[0].value) : '');
}
function renderPresence(online){
el('routing-presence-text').textContent = online ? '在线,参与自动分配' : '离线,不自动接待';
el('routing-presence-dot').dataset.online = String(online);
el('routing-presence-toggle').textContent = online ? '切换离线' : '上线接待';
el('routing-presence-toggle').setAttribute('aria-pressed', String(online));
}
function targets(preserveUnavailable){
if(!snapshot) return;
const group = Number(el('routing-target-group').value);
const previous = el('routing-target-owner').value;
const items = snapshot.agents.filter(agent => agent.online && agent.group_id === group)
.map(agent => ({value:agent.username,label:(agent.name || agent.username)+' · '+agent.active_count+' 个会话'}));
if(preserveUnavailable && previous && !items.some(item=>item.value === previous)){
items.push({value:previous,label:previous+' · 状态已改变,请重新选择'});
}
fill('routing-target-owner',items,'转入队列,等待手动接入');
}
function selectSession(){
const session = snapshot && snapshot.sessions.find(item => String(item.id) === el('routing-session').value);
selectedVersion = session ? session.version : null;
if(session){
el('routing-target-group').value = String(session.group_id);
targets();
el('routing-target-owner').value = session.owner_username || '';
}
}
function render(data){
snapshot = data;
renderPresence(Boolean(data.presence.online));
const mine = data.agents.find(agent => agent.username === options.username);
el('routing-group-label').textContent = mine ? mine.group_name : '综合客服';
el('routing-members').innerHTML = data.agents.length ? data.agents.map(agent =>
'<li><span>'+escape(agent.name || agent.username)+' <small>'+escape(agent.username)+'</small></span>'+
'<span>'+escape(agent.group_name)+' · '+(agent.online ? '在线' : '离线')+' · '+agent.active_count+' 个会话</span></li>').join('') : '<li>暂无客服账号</li>';
const groups = data.groups.map(group => ({value:group.id,label:group.name}));
fill('routing-member-group', groups);
fill('routing-target-group', groups);
fill('routing-member', data.agents.map(agent => ({value:agent.username,label:(agent.name || agent.username)+'('+agent.username+')'})), '选择客服');
fill('routing-session', data.sessions.map(session => ({value:session.id,
label:'#'+session.id+' · '+session.group_name+' · '+(session.owner_name || session.owner_username || '排队中')})), '选择会话');
if(!el('routing-session').value) selectedVersion = null;
targets(true);
const actions = {auto_assign:'自动分配',claim:'手动接入',reassign:'手动改派',queue:'转入分组队列',transfer:'转回队列',close:'结束会话'};
el('routing-events').innerHTML = data.events.length ? data.events.map(event =>
'<li><strong>#'+event.session_id+' · '+escape(actions[event.action] || event.action)+'</strong><span>'+
escape(event.group_name)+' → '+escape(event.owner_username || '等待分配')+'</span><small>'+escape(event.actor === 'system' ? '系统' : event.actor === 'customer' ? '客户' : event.actor)+' · '+escape(event.ctime)+'</small></li>').join('') : '<li>暂无分配记录</li>';
setBusy(busy);
}
async function refresh(afterWrite){
if(busy && afterWrite !== true) return;
const current = ++generation;
try{
const data = await options.request('/api/admin/routing');
if(current !== generation) return;
if(!data || !Array.isArray(data.groups) || !Array.isArray(data.agents) ||
!Array.isArray(data.sessions) || !Array.isArray(data.events) || !data.presence) throw new Error('Invalid routing data');
render(data);
}catch(error){
if(current === generation) message('分配状态加载失败,已保留现有信息;请点击刷新重试。', true);
}
}
async function heartbeat(){
if(busy || beating) return;
beating = true;
const current = generation;
try{
const data = await options.request('/api/admin/routing/presence', {method:'POST',
headers:{'Content-Type':'application/json'},body:'{}'});
if(current !== generation) return;
if(snapshot) snapshot.presence.online = Boolean(data.online);
renderPresence(Boolean(data.online));
await refresh();
}catch(error){
if(current === generation){
el('routing-presence-text').textContent = '接待状态待确认';
message('接待状态同步失败,断线超过 90 秒将停止自动分配;请刷新核对。', true);
}
}finally{ beating = false; }
}
async function setPresence(){
if(busy || !snapshot) return;
++generation;
setBusy(true);
const online = !snapshot.presence.online;
try{
const data = await options.request('/api/admin/routing/presence', {method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify({online})});
snapshot.presence.online = Boolean(data.online);
renderPresence(Boolean(data.online));
message(data.online ? '已上线,空闲时自动接入一个新客户;多接请手动领取。' : '已离线,已接待的会话继续保留。');
if(options.changed) options.changed();
}catch(error){ message('状态更新未确认成功,请点击刷新核对后重试。', true); }
finally{ setBusy(false); }
}
async function write(path, data, success){
if(busy || !snapshot) return false;
++generation;
setBusy(true);
try{
await options.request('/api/admin/routing/'+path, {method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
message(success);
if(options.changed) options.changed();
return true;
}catch(error){
message(error.userMessage || '操作未确认成功,请刷新核对后重试。', true);
return false;
}finally{ await refresh(true); setBusy(false); }
}
async function addGroup(){
const name = el('routing-group-name').value.trim();
if(!name || name.length > 40){ message('请输入 1–40 字的分组名称。', true); return; }
if(await write('groups',{name},'分组已创建,可将客服加入此组。')){
if(el('routing-group-name').value.trim() === name) el('routing-group-name').value = '';
}
}
async function moveMember(){
const username = el('routing-member').value, group_id = Number(el('routing-member-group').value);
if(!username){ message('请先选择客服。', true); return; }
await write('members',{username,group_id},'客服分组已更新,现有会话负责人不变。');
}
async function assign(){
const sid = Number(el('routing-session').value), group_id = Number(el('routing-target-group').value);
if(!sid || selectedVersion === null){ message('请先选择会话。', true); return; }
const version = selectedVersion, username = el('routing-target-owner').value || null;
await write('assign',{sid,group_id,version,username},'会话分配已更新。');
el('routing-session').value = ''; selectedVersion = null;
}
el('routing-presence-toggle').addEventListener('click',setPresence);
el('routing-refresh').addEventListener('click',refresh);
el('routing-session').addEventListener('change',selectSession);
el('routing-target-group').addEventListener('change',()=>targets(false));
el('routing-member').addEventListener('change',()=>{
const member = snapshot && snapshot.agents.find(agent => agent.username === el('routing-member').value);
if(member) el('routing-member-group').value = String(member.group_id);
});
[['routing-group-form',addGroup],['routing-member-form',moveMember],['routing-assign-form',assign]].forEach(([id,handler])=>{
el(id).addEventListener('submit',event=>{event.preventDefault();return handler();});
});
setBusy(false);
return {refresh,heartbeat,setPresence,assign};
}
root.AdminRouting = {create};
})(window);