/* 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 ? '' : '') + items.map(item => '').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 => '
  • '+escape(agent.name || agent.username)+' '+escape(agent.username)+''+ ''+escape(agent.group_name)+' · '+(agent.online ? '在线' : '离线')+' · '+agent.active_count+' 个会话
  • ').join('') : '
  • 暂无客服账号
  • '; 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 => '
  • #'+event.session_id+' · '+escape(actions[event.action] || event.action)+''+ escape(event.group_name)+' → '+escape(event.owner_username || '等待分配')+''+escape(event.actor === 'system' ? '系统' : event.actor === 'customer' ? '客户' : event.actor)+' · '+escape(event.ctime)+'
  • ').join('') : '
  • 暂无分配记录
  • '; 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);