639 lines
22 KiB
HTML
639 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>人工客服工作台</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #f5f5f5;
|
|
height: 100vh;
|
|
display: flex;
|
|
}
|
|
.sidebar {
|
|
width: 300px;
|
|
background: #fff;
|
|
border-right: 1px solid #e0e0e0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.sidebar-header {
|
|
padding: 15px;
|
|
background: #1890ff;
|
|
color: white;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
.session-tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
.session-tab {
|
|
flex: 1;
|
|
padding: 10px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
}
|
|
.session-tab.active {
|
|
border-bottom-color: #1890ff;
|
|
color: #1890ff;
|
|
}
|
|
.session-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
.session-item {
|
|
padding: 12px 15px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
cursor: pointer;
|
|
}
|
|
.session-item:hover {
|
|
background: #f5f5f5;
|
|
}
|
|
.session-item.active {
|
|
background: #e6f7ff;
|
|
}
|
|
.session-item .customer-id {
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
}
|
|
.session-item .last-msg {
|
|
font-size: 12px;
|
|
color: #666;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.session-item .time {
|
|
font-size: 11px;
|
|
color: #999;
|
|
margin-top: 3px;
|
|
}
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
margin-left: 5px;
|
|
}
|
|
.status-pending {
|
|
background: #fff7e6;
|
|
color: #fa8c16;
|
|
}
|
|
.status-manual {
|
|
background: #e6f7ff;
|
|
color: #1890ff;
|
|
}
|
|
.main-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.chat-header {
|
|
padding: 15px;
|
|
background: #fff;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.chat-messages {
|
|
flex: 1;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
background: #f9f9f9;
|
|
}
|
|
.message {
|
|
margin-bottom: 15px;
|
|
display: flex;
|
|
}
|
|
.message.customer {
|
|
justify-content: flex-start;
|
|
}
|
|
.message.ai, .message.manual {
|
|
justify-content: flex-end;
|
|
}
|
|
.message-content {
|
|
max-width: 60%;
|
|
padding: 10px 15px;
|
|
border-radius: 8px;
|
|
position: relative;
|
|
}
|
|
.message.customer .message-content {
|
|
background: #fff;
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
.message.ai .message-content {
|
|
background: #e6f7ff;
|
|
border: 1px solid #91d5ff;
|
|
}
|
|
.message.manual .message-content {
|
|
background: #f6ffed;
|
|
border: 1px solid #b7eb8f;
|
|
}
|
|
.message-sender {
|
|
font-size: 11px;
|
|
color: #999;
|
|
margin-bottom: 3px;
|
|
}
|
|
.message-time {
|
|
font-size: 10px;
|
|
color: #bbb;
|
|
margin-top: 3px;
|
|
}
|
|
.chat-input {
|
|
padding: 15px;
|
|
background: #fff;
|
|
border-top: 1px solid #e0e0e0;
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
.chat-input textarea {
|
|
flex: 1;
|
|
padding: 10px;
|
|
border: 1px solid #d9d9d9;
|
|
border-radius: 4px;
|
|
resize: none;
|
|
height: 60px;
|
|
}
|
|
.chat-input button {
|
|
padding: 10px 20px;
|
|
background: #1890ff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.chat-input button:hover {
|
|
background: #40a9ff;
|
|
}
|
|
.chat-input button:disabled {
|
|
background: #d9d9d9;
|
|
cursor: not-allowed;
|
|
}
|
|
.empty-state {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #999;
|
|
}
|
|
.connection-status {
|
|
padding: 5px 10px;
|
|
font-size: 12px;
|
|
border-radius: 4px;
|
|
}
|
|
.connected {
|
|
background: #f6ffed;
|
|
color: #52c41a;
|
|
}
|
|
.disconnected {
|
|
background: #fff2f0;
|
|
color: #ff4d4f;
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
.actions button {
|
|
padding: 5px 10px;
|
|
border: 1px solid #d9d9d9;
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.actions button:hover {
|
|
border-color: #1890ff;
|
|
color: #1890ff;
|
|
}
|
|
.test-panel {
|
|
position: fixed;
|
|
right: 20px;
|
|
bottom: 20px;
|
|
background: #fff;
|
|
border: 1px solid #d9d9d9;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
width: 300px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
.test-panel h4 {
|
|
margin-bottom: 10px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
.test-panel input, .test-panel textarea {
|
|
width: 100%;
|
|
padding: 8px;
|
|
margin-bottom: 10px;
|
|
border: 1px solid #d9d9d9;
|
|
border-radius: 4px;
|
|
}
|
|
.test-panel button {
|
|
width: 100%;
|
|
padding: 8px;
|
|
background: #52c41a;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.test-panel button:hover {
|
|
background: #73d13d;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">
|
|
客服工作台 <span id="csId">CS_001</span>
|
|
</div>
|
|
<div class="session-tabs">
|
|
<div class="session-tab active" data-status="PENDING" onclick="switchTab('PENDING')">
|
|
待接入 (<span id="pendingCount">0</span>)
|
|
</div>
|
|
<div class="session-tab" data-status="MANUAL" onclick="switchTab('MANUAL')">
|
|
进行中 (<span id="manualCount">0</span>)
|
|
</div>
|
|
</div>
|
|
<div class="session-list" id="sessionList">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<div id="chatArea" style="display: none; height: 100%; flex-direction: column;">
|
|
<div class="chat-header">
|
|
<div>
|
|
<strong id="currentCustomer">-</strong>
|
|
<span class="status-badge" id="currentStatus">-</span>
|
|
</div>
|
|
<div class="actions">
|
|
<button onclick="acceptSession()" id="acceptBtn">接入会话</button>
|
|
<button onclick="closeSession()" id="closeBtn">结束会话</button>
|
|
</div>
|
|
</div>
|
|
<div class="chat-messages" id="chatMessages">
|
|
</div>
|
|
<div class="chat-input">
|
|
<textarea id="messageInput" placeholder="输入消息..."></textarea>
|
|
<button onclick="sendMessage()" id="sendBtn" disabled>发送</button>
|
|
</div>
|
|
</div>
|
|
<div id="emptyState" class="empty-state">
|
|
<div style="text-align: center;">
|
|
<p>请从左侧选择一个会话</p>
|
|
<p style="margin-top: 10px; font-size: 12px;">WebSocket: <span id="wsStatus" class="connection-status disconnected">未连接</span></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-panel">
|
|
<h4>🧪 模拟客户消息</h4>
|
|
<input type="text" id="testCustomerId" placeholder="客户ID" value="test_customer_001">
|
|
<input type="text" id="testKfId" placeholder="客服账号ID" value="test_kf_001">
|
|
<textarea id="testContent" placeholder="消息内容"></textarea>
|
|
<button onclick="sendTestMessage()">发送测试消息</button>
|
|
<button onclick="triggerTransfer()" style="margin-top: 5px; background: #fa8c16;">触发转人工</button>
|
|
</div>
|
|
|
|
<script>
|
|
let ws = null;
|
|
let currentSessionId = null;
|
|
let currentStatus = null;
|
|
let csId = 'CS_001';
|
|
const baseUrl = window.location.origin;
|
|
|
|
function connectWebSocket() {
|
|
const wsUrl = baseUrl.replace('http', 'ws') + '/ws/cs/' + csId;
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = function() {
|
|
document.getElementById('wsStatus').className = 'connection-status connected';
|
|
document.getElementById('wsStatus').textContent = '已连接';
|
|
console.log('WebSocket已连接');
|
|
};
|
|
|
|
ws.onclose = function() {
|
|
document.getElementById('wsStatus').className = 'connection-status disconnected';
|
|
document.getElementById('wsStatus').textContent = '已断开';
|
|
console.log('WebSocket已断开');
|
|
setTimeout(connectWebSocket, 3000);
|
|
};
|
|
|
|
ws.onmessage = function(event) {
|
|
const data = JSON.parse(event.data);
|
|
console.log('收到消息:', data);
|
|
handleWebSocketMessage(data);
|
|
};
|
|
|
|
ws.onerror = function(error) {
|
|
console.error('WebSocket错误:', error);
|
|
};
|
|
}
|
|
|
|
function handleWebSocketMessage(data) {
|
|
switch(data.type) {
|
|
case 'new_pending_session':
|
|
alert('有新的待接入会话!');
|
|
loadSessions();
|
|
break;
|
|
case 'new_message':
|
|
case 'customer_message':
|
|
if (currentSessionId === data.sessionId) {
|
|
addMessage('customer', data.content, data.timestamp);
|
|
}
|
|
loadSessions();
|
|
break;
|
|
case 'session_accepted':
|
|
if (currentSessionId === data.sessionId) {
|
|
currentStatus = 'MANUAL';
|
|
updateChatHeader();
|
|
document.getElementById('sendBtn').disabled = false;
|
|
document.getElementById('acceptBtn').disabled = true;
|
|
}
|
|
break;
|
|
case 'session_closed':
|
|
if (currentSessionId === data.sessionId) {
|
|
alert('会话已结束');
|
|
currentSessionId = null;
|
|
showEmptyState();
|
|
}
|
|
loadSessions();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function switchTab(status) {
|
|
document.querySelectorAll('.session-tab').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
if (tab.dataset.status === status) {
|
|
tab.classList.add('active');
|
|
}
|
|
});
|
|
loadSessions(status);
|
|
}
|
|
|
|
async function loadSessions(status = 'PENDING') {
|
|
try {
|
|
const response = await fetch(baseUrl + '/api/sessions?status=' + status);
|
|
const result = await response.json();
|
|
|
|
if (result.code === 200) {
|
|
renderSessionList(result.data, status);
|
|
|
|
if (status === 'PENDING') {
|
|
document.getElementById('pendingCount').textContent = result.data.length;
|
|
} else {
|
|
document.getElementById('manualCount').textContent = result.data.length;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('加载会话列表失败:', error);
|
|
}
|
|
}
|
|
|
|
function renderSessionList(sessions, status) {
|
|
const list = document.getElementById('sessionList');
|
|
list.innerHTML = '';
|
|
|
|
sessions.forEach(session => {
|
|
const item = document.createElement('div');
|
|
item.className = 'session-item' + (currentSessionId === session.sessionId ? ' active' : '');
|
|
item.onclick = () => selectSession(session);
|
|
|
|
const time = session.lastMessageTime ? new Date(session.lastMessageTime).toLocaleString() : '-';
|
|
|
|
item.innerHTML = `
|
|
<div class="customer-id">
|
|
${session.customerId}
|
|
<span class="status-badge status-${session.status.toLowerCase()}">${session.status}</span>
|
|
</div>
|
|
<div class="last-msg">${session.lastMessage || '暂无消息'}</div>
|
|
<div class="time">${time}</div>
|
|
`;
|
|
|
|
list.appendChild(item);
|
|
});
|
|
|
|
if (sessions.length === 0) {
|
|
list.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">暂无会话</div>';
|
|
}
|
|
}
|
|
|
|
async function selectSession(session) {
|
|
currentSessionId = session.sessionId;
|
|
currentStatus = session.status;
|
|
|
|
document.querySelectorAll('.session-item').forEach(item => {
|
|
item.classList.remove('active');
|
|
});
|
|
event.currentTarget.classList.add('active');
|
|
|
|
document.getElementById('emptyState').style.display = 'none';
|
|
document.getElementById('chatArea').style.display = 'flex';
|
|
|
|
updateChatHeader();
|
|
await loadHistory();
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({
|
|
type: 'bind_session',
|
|
sessionId: currentSessionId
|
|
}));
|
|
}
|
|
}
|
|
|
|
function updateChatHeader() {
|
|
document.getElementById('currentCustomer').textContent = currentSessionId;
|
|
|
|
const statusBadge = document.getElementById('currentStatus');
|
|
statusBadge.textContent = currentStatus;
|
|
statusBadge.className = 'status-badge status-' + currentStatus.toLowerCase();
|
|
|
|
document.getElementById('acceptBtn').disabled = currentStatus !== 'PENDING';
|
|
document.getElementById('sendBtn').disabled = currentStatus !== 'MANUAL';
|
|
document.getElementById('closeBtn').disabled = currentStatus !== 'MANUAL';
|
|
}
|
|
|
|
async function loadHistory() {
|
|
try {
|
|
const response = await fetch(baseUrl + '/api/sessions/' + currentSessionId + '/history');
|
|
const result = await response.json();
|
|
|
|
if (result.code === 200) {
|
|
const container = document.getElementById('chatMessages');
|
|
container.innerHTML = '';
|
|
|
|
result.data.forEach(msg => {
|
|
addMessage(msg.senderType, msg.content, msg.createdAt);
|
|
});
|
|
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
} catch (error) {
|
|
console.error('加载历史消息失败:', error);
|
|
}
|
|
}
|
|
|
|
function addMessage(senderType, content, timestamp) {
|
|
const container = document.getElementById('chatMessages');
|
|
const msg = document.createElement('div');
|
|
msg.className = 'message ' + senderType;
|
|
|
|
const senderName = senderType === 'customer' ? '客户' :
|
|
senderType === 'ai' ? 'AI客服' : '人工客服';
|
|
const time = timestamp ? new Date(timestamp).toLocaleString() : new Date().toLocaleString();
|
|
|
|
msg.innerHTML = `
|
|
<div class="message-content">
|
|
<div class="message-sender">${senderName}</div>
|
|
<div>${content}</div>
|
|
<div class="message-time">${time}</div>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(msg);
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
|
|
async function acceptSession() {
|
|
if (!currentSessionId) return;
|
|
|
|
try {
|
|
const response = await fetch(baseUrl + '/api/sessions/' + currentSessionId + '/accept', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ csId: csId })
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.code === 200) {
|
|
currentStatus = 'MANUAL';
|
|
updateChatHeader();
|
|
loadSessions('PENDING');
|
|
loadSessions('MANUAL');
|
|
} else {
|
|
alert('接入失败: ' + result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('接入会话失败:', error);
|
|
}
|
|
}
|
|
|
|
async function sendMessage() {
|
|
if (!currentSessionId || currentStatus !== 'MANUAL') return;
|
|
|
|
const content = document.getElementById('messageInput').value.trim();
|
|
if (!content) return;
|
|
|
|
try {
|
|
const response = await fetch(baseUrl + '/api/sessions/' + currentSessionId + '/message', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content: content, msgType: 'text' })
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.code === 200) {
|
|
addMessage('manual', content);
|
|
document.getElementById('messageInput').value = '';
|
|
} else {
|
|
alert('发送失败: ' + result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('发送消息失败:', error);
|
|
}
|
|
}
|
|
|
|
async function closeSession() {
|
|
if (!currentSessionId) return;
|
|
|
|
try {
|
|
const response = await fetch(baseUrl + '/api/sessions/' + currentSessionId + '/close', {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.code === 200) {
|
|
currentSessionId = null;
|
|
showEmptyState();
|
|
loadSessions('PENDING');
|
|
loadSessions('MANUAL');
|
|
}
|
|
} catch (error) {
|
|
console.error('结束会话失败:', error);
|
|
}
|
|
}
|
|
|
|
function showEmptyState() {
|
|
document.getElementById('emptyState').style.display = 'flex';
|
|
document.getElementById('chatArea').style.display = 'none';
|
|
}
|
|
|
|
async function sendTestMessage() {
|
|
const customerId = document.getElementById('testCustomerId').value;
|
|
const kfId = document.getElementById('testKfId').value;
|
|
const content = document.getElementById('testContent').value;
|
|
|
|
if (!content) {
|
|
alert('请输入消息内容');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(baseUrl + '/test/send-message?customerId=' + customerId + '&kfId=' + kfId + '&content=' + encodeURIComponent(content), {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.code === 200) {
|
|
alert('消息已发送!');
|
|
document.getElementById('testContent').value = '';
|
|
setTimeout(() => loadSessions('PENDING'), 500);
|
|
setTimeout(() => loadSessions('MANUAL'), 500);
|
|
}
|
|
} catch (error) {
|
|
console.error('发送测试消息失败:', error);
|
|
}
|
|
}
|
|
|
|
async function triggerTransfer() {
|
|
const customerId = document.getElementById('testCustomerId').value;
|
|
const kfId = document.getElementById('testKfId').value;
|
|
|
|
try {
|
|
const response = await fetch(baseUrl + '/test/trigger-transfer?customerId=' + customerId + '&kfId=' + kfId, {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.code === 200) {
|
|
alert('已触发转人工!');
|
|
setTimeout(() => loadSessions('PENDING'), 500);
|
|
}
|
|
} catch (error) {
|
|
console.error('触发转人工失败:', error);
|
|
}
|
|
}
|
|
|
|
document.getElementById('messageInput').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
connectWebSocket();
|
|
loadSessions('PENDING');
|
|
loadSessions('MANUAL');
|
|
</script>
|
|
</body>
|
|
</html>
|