Files
BuYuHouDuan/AdminSrc/AdminHost/wwwroot/admin/dist/views/customer/service.html
T

1354 lines
45 KiB
HTML
Raw Normal View History

2026-10-12 16:22:04 +08:00
<title>在线客服</title>
<div class="layui-card layadmin-header">
<div class="layui-breadcrumb" lay-filter="breadcrumb">
<a lay-href="">主页</a>
<a><cite>在线客服</cite></a>
</div>
</div>
<div style="padding: 10px 20px; background: #fff; border-bottom: 1px solid #e6e6e6;">
<span style="font-weight: bold;">选择客服:</span>
<select id="customer-select" style="padding: 5px 10px; border: 1px solid #ddd; border-radius: 4px; margin-right: 10px;" onchange="onCustomerChange()">
<option value="">请选择客服账号</option>
</select>
<span style="font-weight: bold; margin-left: 10px;">状态:</span>
<button id="status-btn" class="layui-btn layui-btn-xs" onclick="toggleCustomerStatus()">设为在线</button>
<span id="status-display" class="layui-badge" style="margin-left: 10px;">离线</span>
<span style="margin-left: 20px; color: #666; font-size: 12px;">(请先在"客服管理"中添加客服账号)</span>
</div>
<div class="layui-fluid" style="padding: 0; height: calc(100vh - 130px); overflow: hidden;">
<div class="layui-row layui-col-space0" style="height: 100%;">
<!-- 左侧会话列表 -->
<div class="layui-col-md3" style="height: 100%;">
<div class="conversation-list">
<div class="conversation-header">
<span><i class="layui-icon layui-icon-dialogue"></i> 会话列表</span>
<span class="layui-badge layui-bg-orange" id="online-count">0</span>
</div>
<div class="conversation-list-container" id="conversation-list-container"></div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div class="layui-col-md9" style="height: 100%; display: flex; flex-direction: column;">
<div class="chat-area" style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
<div class="chat-header" style="flex-shrink: 0;">
<span class="chat-header-name" id="current-user">请选择会话</span>
<span id="current-status" class="layui-badge" style="margin-left: 10px;"></span>
</div>
<!-- 消息展示区域 -->
<div id="message-container" class="message-container" style="flex: 1; min-height: 100px;">
<div class="empty-state">
<i class="layui-icon layui-icon-dialogue"></i>
<p>请选择左侧会话开始聊天</p>
</div>
</div>
<!-- 发送消息区域 -->
<div class="input-area" style="flex-shrink: 0;">
<div class="quick-reply">
<span onclick="insertText('您好,请问有什么可以帮助您?')">您好</span>
<span onclick="insertText('请稍等,我为您查询一下')">稍等</span>
<span onclick="insertText('感谢您的咨询,祝您生活愉快!')">结束</span>
<span onclick="document.getElementById('image-upload').click()">
<i class="layui-icon layui-icon-upload"></i> 图片
</span>
<input type="file" id="image-upload" accept="image/*" style="display: none;" onchange="handleImageUpload(this)">
</div>
<div class="input-wrapper">
<textarea id="message-input" class="message-textarea" placeholder="输入消息内容..." disabled></textarea>
<button id="send-btn" class="send-btn" disabled onclick="sendMessage()">发送</button>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* 整体布局 */
html, body, .layui-layout-admin, .layui-body {
overflow: hidden !important;
height: 100%;
}
.layui-fluid {
overflow: hidden;
height: calc(100vh - 60px);
padding: 0;
}
/* 隐藏所有滚动条 */
* {
scrollbar-width: none;
-ms-overflow-style: none;
}
*::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
/* 会话列表样式 */
.conversation-list {
height: 100%;
background: #fff;
border-right: 1px solid #e6e6e6;
}
.conversation-header {
padding: 12px 15px;
background: #009688;
color: #fff;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
}
.conversation-list-container {
height: calc(100% - 45px);
overflow-y: auto;
overflow-x: hidden;
}
/* 隐藏滚动条 */
.conversation-list-container::-webkit-scrollbar { width: 0; }
.conversation-item {
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
}
.conversation-item:hover {
background: #f5f5f5;
}
.conversation-item.active {
background: #e6f7f5;
border-left: 3px solid #009688;
}
.conversation-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(135deg, #009688 0%, #26a69a 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 16px;
margin-right: 12px;
flex-shrink: 0;
position: relative;
}
.conversation-info {
flex: 1;
min-width: 0;
}
.conversation-name {
font-weight: 600;
font-size: 14px;
color: #333;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-preview {
font-size: 12px;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-meta {
text-align: right;
flex-shrink: 0;
margin-left: 10px;
}
.conversation-time {
font-size: 11px;
color: #bbb;
margin-bottom: 6px;
}
/* 未读标记 */
.unread-dot {
position: absolute;
top: 2px;
right: 2px;
width: 8px;
height: 8px;
background: #ff5722;
border-radius: 50%;
}
.unread-badge {
display: inline-block;
padding: 2px 6px;
background: #ff5722;
color: #fff;
border-radius: 8px;
font-size: 10px;
font-weight: 600;
}
/* 聊天区域 */
.chat-area {
background: linear-gradient(180deg, #fafbfc 0%, #f0f2f5 100%);
height: 100%;
}
.chat-header {
padding: 16px 24px;
background: #fff;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.chat-header-name {
font-size: 18px;
font-weight: 600;
color: #333;
}
/* 消息区域 */
.message-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 20px;
display: flex;
flex-direction: column;
}
.message-container::-webkit-scrollbar { width: 0; }
.message-wrapper {
display: flex;
margin-bottom: 16px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message-wrapper.user {
flex-direction: row;
}
.message-wrapper.customer {
flex-direction: row-reverse;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
}
.message-wrapper.user .message-avatar {
background: linear-gradient(135deg, #009688 0%, #26a69a 100%);
color: #fff;
margin-right: 12px;
}
.message-wrapper.customer .message-avatar {
background: linear-gradient(135deg, #ff9800 0%, #ffc107 100%);
color: #fff;
margin-left: 12px;
}
.message-content {
max-width: 65%;
min-width: 60px;
}
.message-bubble {
padding: 12px 18px;
border-radius: 16px;
font-size: 14px;
line-height: 1.6;
word-break: break-word;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.message-wrapper.user .message-bubble {
background: #fff;
color: #333;
border-bottom-left-radius: 4px;
}
.message-wrapper.customer .message-bubble {
background: linear-gradient(135deg, #009688 0%, #00796b 100%);
color: #fff;
border-bottom-right-radius: 4px;
}
.message-time {
font-size: 11px;
color: #bbb;
margin-top: 6px;
padding: 0 4px;
}
.message-wrapper.user .message-time {
text-align: left;
}
.message-wrapper.customer .message-time {
text-align: right;
}
.message-image {
max-width: 250px;
max-height: 200px;
border-radius: 12px;
cursor: pointer;
transition: transform 0.2s;
}
.message-image:hover {
transform: scale(1.02);
}
/* 输入区域 */
.input-area {
background: #fff;
padding: 16px 20px;
border-top: 1px solid #eee;
box-shadow: 0 -2px 10px rgba(0,0,0,0.03);
}
.input-wrapper {
display: flex !important;
gap: 12px;
align-items: flex-end;
}
.message-textarea {
flex: 1;
border: 2px solid #e8e8e8;
border-radius: 12px;
padding: 12px 16px;
font-size: 14px;
resize: none;
outline: none;
transition: border-color 0.2s;
min-height: 50px;
max-height: 100px;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.message-textarea:focus {
border-color: #009688;
}
.send-btn {
width: 80px !important;
height: 50px !important;
background: linear-gradient(135deg, #009688 0%, #00796b 100%) !important;
color: #fff !important;
border: none !important;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0,150,136,0.3);
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
}
.send-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,150,136,0.4);
}
.send-btn:disabled {
background: #ccc;
cursor: not-allowed;
box-shadow: none;
}
.quick-reply {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.quick-reply span {
padding: 6px 14px;
background: #f0f5f4;
border-radius: 16px;
font-size: 12px;
color: #666;
cursor: pointer;
transition: all 0.2s;
}
.quick-reply span:hover {
background: #e0f2f1;
color: #009688;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state i {
font-size: 60px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state p {
font-size: 15px;
}
</style>
<script>
layui.use(['admin', 'table', 'form'], function () {
var $ = layui.$,
admin = layui.admin,
table = layui.table,
setter = layui.setter,
form = layui.form,
customerApiUrl = setter.customerApiUrl || setter.baseurl;
// 当前登录的客服信息
var currentCustomerId = '';
var currentCustomerName = '';
var currentConversationId = '';
var conversationList = [];
var messageCache = {};
var customerList = [];
var readConversations = new Set(); // 已读会话集合
var unreadCounts = {}; // 每个会话的未读消息计数 { conversationId: number }
// 初始化客服信息
function initCustomerInfo() {
// 从后台获取客服列表
loadCustomerList();
}
// 加载客服列表
function loadCustomerList() {
admin.req({
method: "get",
url: setter.baseurl + "adminapi/customer/list?pageIndex=1&pageSize=100",
headers: {
'token': layui.data(setter.tableName)[setter.request.tokenName]
},
done: function (res) {
console.log('客服列表响应:', res);
// 处理响应格式
var list = [];
if (res && res.code === 0) {
// 直接是数组
if (Array.isArray(res.data)) {
list = res.data;
}
// 嵌套结构 { data: { data: [...] } }
else if (res.data && res.data.data && Array.isArray(res.data.data)) {
list = res.data.data;
}
}
customerList = list;
renderCustomerSelect();
},
fail: function(err) {
console.error('加载客服列表失败:', err);
customerList = [];
renderCustomerSelect();
}
});
}
// 渲染客服选择下拉框
function renderCustomerSelect() {
var select = document.getElementById('customer-select');
if (!select) return;
select.innerHTML = '<option value="">请选择客服账号</option>';
if (!customerList || !Array.isArray(customerList)) {
return;
}
customerList.forEach(function(customer) {
if (!customer) return;
var userId = customer.userId || '';
var userName = customer.userName || '未知';
var option = document.createElement('option');
option.value = userId;
option.textContent = userName + ' (' + userId + ')';
option.setAttribute('data-name', userName);
select.appendChild(option);
});
}
// 客服选择变更
window.onCustomerChange = function() {
var select = document.getElementById('customer-select');
var option = select.options[select.selectedIndex];
currentCustomerId = select.value;
currentCustomerName = option.getAttribute('data-name') || '客服';
// 重置状态
isCustomerOnline = false;
updateStatusDisplay();
// 如果选择了客服,检查在线状态并加入客服队列
if (currentCustomerId) {
checkCustomerStatus();
loadConversations();
// 加入客服队列以接收新会话通知
if (signalRConnection && signalRCore && signalRConnection.state === signalRCore.HubConnectionState.Connected) {
signalRConnection.invoke("JoinCustomerQueue", currentCustomerId)
.then(function() {
console.log("已加入客服队列:", currentCustomerId);
})
.catch(function(err) {
console.error("加入客服队列失败:", err);
});
}
}
}
// 加载会话列表
// 加载会话列表
function loadConversations() {
if (!currentCustomerId) {
conversationList = [];
renderConversationList();
var countEl = document.getElementById('online-count');
if (countEl) countEl.textContent = '0';
return;
}
admin.req({
method: "get",
url: customerApiUrl + "customerapi/customer/conversations?customerId=" + currentCustomerId,
headers: {
'token': layui.data(setter.tableName)[setter.request.tokenName]
},
done: function (res) {
if (res.code === 0 && res.data) {
var newData = res.data;
// 数据未变化时跳过重渲染,避免闪烁
var oldJson = JSON.stringify(conversationList);
var newJson = JSON.stringify(newData);
if (oldJson === newJson) return;
conversationList = newData;
// 用服务端返回的 unreadCount 初始化/更新未读数
conversationList.forEach(function(conv) {
if (!readConversations.has(conv.id)) {
var serverUnread = conv.unreadCount || conv.UnreadCount || 0;
// 服务端有数据就用服务端的(刷新时重新同步)
if (serverUnread > 0) {
unreadCounts[conv.id] = serverUnread;
} else if (unreadCounts[conv.id] === undefined) {
unreadCounts[conv.id] = 0;
}
}
});
renderConversationList();
updateOnlineCount();
}
}
});
}
// 渲染会话列表
function renderConversationList() {
var container = document.getElementById('conversation-list-container');
if (!container) {
console.error('conversation-list-container not found');
return;
}
if (!conversationList || conversationList.length === 0) {
container.innerHTML = '<div class="empty-state"><i class="layui-icon layui-icon-dialogue"></i><p>暂无会话</p></div>';
return;
}
// 按用户ID分组,同一个用户的会话合并显示最新的一条
var userMap = {};
conversationList.forEach(function(conv) {
// 处理字段名大小写问题
var uid = conv.userId || conv.userid || conv.id;
var uname = conv.userName || conv.username || '未知用户';
var ctime = conv.createTime || conv.createtime || new Date();
var sta = conv.status || 0;
if (!userMap[uid]) {
userMap[uid] = {
userId: uid,
userName: uname,
conversationId: conv.id,
createTime: ctime,
status: sta,
unreadCount: 0,
lastMessage: null
};
}
// 更新最新会话(按时间取最新的)
if (new Date(ctime) > new Date(userMap[uid].createTime)) {
userMap[uid].conversationId = conv.id;
userMap[uid].createTime = ctime;
userMap[uid].status = sta;
}
// 累加各会话的未读计数(从 unreadCounts 读取,仅统计未读会话)
if (!readConversations.has(conv.id)) {
userMap[uid].unreadCount += (unreadCounts[conv.id] || 0);
}
// 更新最后一条消息
if (messageCache[conv.id] && messageCache[conv.id].length > 0) {
var lastMsg = messageCache[conv.id][messageCache[conv.id].length - 1];
if (!userMap[uid].lastMessage || new Date(lastMsg.createTime || lastMsg.createtime) > new Date(userMap[uid].lastMessage.createTime || userMap[uid].lastMessage.createtime)) {
userMap[uid].lastMessage = lastMsg;
}
}
});
var groupedList = Object.values(userMap);
// 按时间排序,最新的在前面
groupedList.sort(function(a, b) {
return new Date(b.createTime) - new Date(a.createTime);
});
var html = '';
groupedList.forEach(function(conv) {
var hasUnread = conv.unreadCount > 0;
var lastMsgText = conv.lastMessage ? (conv.lastMessage.contentType === 2 || conv.lastMessage.contenttype === 2 ? '[图片]' : conv.lastMessage.content || '') : '暂无消息';
var isActive = conv.conversationId === currentConversationId;
html += '<div class="conversation-item' + (isActive ? ' active' : '') + '" conversation-id="' + conv.conversationId + '" onclick="selectConversation(\'' + conv.conversationId + '\', \'' + conv.userId + '\')">' +
'<div class="conversation-avatar">' + (conv.userName ? conv.userName.substr(0, 1) : '?') + (hasUnread ? '<span class="unread-dot"></span>' : '') + '</div>' +
'<div class="conversation-info">' +
'<div class="conversation-name">' + (conv.userName || '未知用户') + '</div>' +
'<div class="conversation-preview">' + lastMsgText + '</div>' +
'</div>' +
'<div class="conversation-meta">' +
'<div class="conversation-time">' + formatTime(conv.createTime) + '</div>' +
(hasUnread ? '<span class="unread-badge">' + conv.unreadCount + '</span>' : '') +
'</div>' +
'</div>';
});
container.innerHTML = html;
}
// 选择会话
window.selectConversation = function(conversationId, userId) {
// 移除其他选中状态
document.querySelectorAll('.conversation-item').forEach(function(el) {
el.classList.remove('active');
});
// 选中当前会话
var item = document.querySelector('[conversation-id="' + conversationId + '"]');
if (item) item.classList.add('active');
// 更新当前会话ID
currentConversationId = conversationId;
// 标记该用户所有会话为已读,清零未读计数
conversationList.forEach(function(c) {
if ((c.userId || c.userid) === userId) {
readConversations.add(c.id);
unreadCounts[c.id] = 0;
}
});
// 刷新会话列表显示
renderConversationList();
// 更新顶部未读总数
updateOnlineCount();
// 获取会话信息
var conv = conversationList.find(function(c) { return c.id === conversationId; });
if (conv) {
document.getElementById('current-user').textContent = conv.userName || conv.username || '未知用户';
var statusText = conv.status === 0 ? '等待中' : (conv.status === 1 ? '进行中' : '已结束');
var statusBadge = document.getElementById('current-status');
statusBadge.textContent = statusText;
statusBadge.className = 'layui-badge ' + (conv.status === 1 ? 'layui-bg-green' : (conv.status === 0 ? 'layui-bg-orange' : 'layui-bg-gray'));
}
// 启用输入框
document.getElementById('message-input').disabled = false;
document.getElementById('send-btn').disabled = false;
// 通过SignalR加入会话组
if (signalRConnection && signalRCore && signalRConnection.state === signalRCore.HubConnectionState.Connected) {
// 加入当前会话
signalRConnection.invoke('JoinConversation', conversationId)
.then(function() {
console.log('已加入会话组:', conversationId);
})
.catch(function(err) {
console.error('加入会话组失败:', err);
});
// 加入该用户的所有会话
var userConversations = conversationList.filter(function(c) { return c.userId === userId; });
userConversations.forEach(function(c) {
if (c.id !== conversationId) {
signalRConnection.invoke('JoinConversation', c.id)
.catch(function(err) {
console.error('加入会话组失败:', err);
});
}
});
}
// 加载消息记录
loadMessages(conversationId, userId);
};
// 加载消息记录
function loadMessages(conversationId, userId) {
// 加载消息时标记为已读,清零该用户所有会话的未读计数
conversationList.forEach(function(c) {
if ((c.userId || c.userid) === userId) {
readConversations.add(c.id);
unreadCounts[c.id] = 0;
}
});
// 通知后端标记已读(该用户所有会话)
$.ajax({
url: customerApiUrl + "customerapi/customer/mark-read",
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ customerId: currentCustomerId, userId: userId }),
success: function(res) {
console.log('已标记用户所有会话为已读:', userId);
// 后端标记成功后,重新加载会话列表让未读数归零
loadConversations();
},
error: function(err) {
console.error('标记已读失败:', err);
}
});
// 更新未读总数
updateOnlineCount();
renderConversationList();
// 加载当前会话消息
loadConversationMessages(conversationId, function() {
// 如果有userId,加载该用户所有会话的消息
if (userId) {
var userConversations = conversationList.filter(function(c) {
return (c.userId || c.userid) === userId;
});
var loadedCount = 0;
var totalToLoad = userConversations.length - 1;
if (totalToLoad <= 0) {
showAllUserMessages(userId);
} else {
userConversations.forEach(function(c) {
if (c.id !== conversationId) {
loadConversationMessages(c.id, function() {
loadedCount++;
if (loadedCount >= totalToLoad) {
showAllUserMessages(userId);
}
});
}
});
}
}
// 兜底:确保当前会话消息一定渲染,不受userId分组异常影响
if (messageCache[conversationId] && messageCache[conversationId].length > 0) {
renderMessages(messageCache[conversationId]);
}
});
}
// 加载单个会话的消息
function loadConversationMessages(convId, callback) {
admin.req({
method: "get",
url: customerApiUrl + "customerapi/customer/messages/" + convId,
headers: {
'token': layui.data(setter.tableName)[setter.request.tokenName]
},
done: function (res) {
if (res.code === 0 && res.data) {
messageCache[convId] = res.data;
renderConversationList();
if (callback) callback();
}
}
});
}
// 显示用户所有消息(合并排序)
function showAllUserMessages(userId) {
var userConversations = conversationList.filter(function(c) {
return (c.userId || c.userid) === userId;
});
var allMessages = [];
userConversations.forEach(function(c) {
if (messageCache[c.id]) {
allMessages = allMessages.concat(messageCache[c.id]);
}
});
// 按时间排序
allMessages.sort(function(a, b) {
return new Date(a.createTime || a.createtime) - new Date(b.createTime || b.createtime);
});
renderMessages(allMessages);
}
// 渲染消息
function renderMessages(messages) {
var container = document.getElementById('message-container');
if (!container) return;
if (!messages || messages.length === 0) {
container.innerHTML = '<div class="empty-state"><i class="layui-icon layui-icon-dialogue"></i><p>暂无消息记录</p></div>';
return;
}
var html = '';
messages.forEach(function(msg) {
// 处理字段名大小写
var senderType = msg.senderType || msg.sendertype || 2;
var senderName = msg.senderName || msg.sendername || '未知';
var contentType = msg.contentType || msg.contenttype || 1;
var content = msg.content || msg.content || '';
var createTime = msg.createTime || msg.createtime || new Date();
var isCustomer = senderType === 2;
var isUser = senderType === 1;
var time = '';
if (createTime) {
var date = new Date(createTime);
var utc = date.getTime() + (date.getTimezoneOffset() * 60000);
var beijingDate = new Date(utc + (3600000 * 8));
time = beijingDate.toLocaleString('zh-CN', { hour12: false });
}
var msgContent = contentType === 2
? '<img src="' + content + '" class="message-image" onclick="viewImage(\'' + content + '\')">'
: content;
var avatarText = senderName ? senderName.substr(0, 1) : '?';
html += '<div class="message-wrapper ' + (isCustomer ? 'customer' : 'user') + '">' +
'<div class="message-avatar">' + avatarText + '</div>' +
'<div class="message-content">' +
'<div class="message-bubble">' + msgContent + '</div>' +
'<div class="message-time">' + senderName + ' · ' + time + '</div>' +
'</div>' +
'</div>';
});
container.innerHTML = html;
container.scrollTop = container.scrollHeight;
}
// 查看大图
window.viewImage = function(url) {
layer.open({
type: 1,
title: '图片预览',
area: ['800px', '600px'],
content: '<div style="text-align:center;padding:20px;"><img src="' + url + '" style="max-width:100%;max-height:100%;"></div>'
});
};
// 发送消息 - 优先SignalR,备用HTTP
function sendMessage() {
if (!currentConversationId) {
layer.msg('请选择会话');
return;
}
var content = document.getElementById('message-input').value.trim();
if (!content) {
layer.msg('请输入消息内容');
return;
}
// 优先使用SignalR发送
if (signalRConnection && signalRCore && signalRConnection.state === signalRCore.HubConnectionState.Connected) {
console.log('通过SignalR发送消息到会话:', currentConversationId);
signalRConnection.invoke('SendMessage', currentConversationId, 2, currentCustomerId, currentCustomerName, null, 1, content,"")
.then(function() {
document.getElementById('message-input').value = '';
// 不手动添加消息,等待SignalR回调 OnMessageReceived 来更新UI
})
.catch(function(err) {
console.error('SignalR发送失败,尝试HTTP:', err);
sendMessageByHttp(content);
});
} else {
sendMessageByHttp(content);
}
}
// 通过HTTP API发送消息
function sendMessageByHttp(content) {
admin.req({
method: "post",
contentType: "application/json;charset=UTF-8",
headers: {
'token': layui.data(setter.tableName)[setter.request.tokenName]
},
url: customerApiUrl + "customerapi/customer/message",
data: JSON.stringify({
conversationId: currentConversationId,
senderType: 2,
senderId: currentCustomerId,
senderName: currentCustomerName,
contentType: 1,
content: content
}),
done: function (res) {
if (res.code === 0) {
document.getElementById('message-input').value = '';
var newMsg = {
conversationId: currentConversationId,
senderType: 2,
senderId: currentCustomerId,
senderName: currentCustomerName,
contentType: 1,
content: content,
createTime: new Date()
};
if (!messageCache[currentConversationId]) {
messageCache[currentConversationId] = [];
}
messageCache[currentConversationId].push(newMsg);
renderMessages(messageCache[currentConversationId]);
} else {
layer.msg(res.info || '发送失败');
}
}
});
}
// 快捷输入
window.insertText = function(text) {
var input = document.getElementById('message-input');
input.value += text;
};
// 图片上传处理
window.handleImageUpload = function(input) {
if (!currentConversationId) {
layer.msg('请先选择会话');
input.value = '';
return;
}
var file = input.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
layer.msg('请选择图片文件');
input.value = '';
return;
}
var formData = new FormData();
formData.append('file', file);
layer.msg('正在上传图片...');
admin.req({
method: 'post',
url: customerApiUrl + 'customerapi/ImageUpload/upload',
data: formData,
processData: false,
contentType: false,
done: function(res) {
if (res.code === 0) {
var imageUrl = res.path || res.data;
console.log('图片上传成功:', imageUrl);
// 通过SignalR发送图片
if (signalRConnection && signalRCore && signalRConnection.state === signalRCore.HubConnectionState.Connected) {
signalRConnection.invoke('SendMessage', currentConversationId, 2, currentCustomerId, currentCustomerName, null, 2, imageUrl,"")
.then(function() {
console.log('图片消息发送成功');
})
.catch(function(err) {
console.error('发送失败,尝试HTTP:', err);
sendImageByHttp(imageUrl);
});
} else {
sendImageByHttp(imageUrl);
}
} else {
layer.msg(res.info || '图片上传失败');
}
input.value = '';
},
fail: function(res) {
layer.msg('图片上传失败');
input.value = '';
}
});
};
// 通过HTTP发送图片
function sendImageByHttp(imageUrl) {
admin.req({
method: "post",
contentType: "application/json;charset=UTF-8",
headers: {
'token': layui.data(setter.tableName)[setter.request.tokenName]
},
url: customerApiUrl + "customerapi/customer/message",
data: JSON.stringify({
conversationId: currentConversationId,
senderType: 2,
senderId: currentCustomerId,
senderName: currentCustomerName,
contentType: 2,
content: imageUrl
}),
done: function(res) {
if (res.code === 0) {
var newMsg = {
conversationId: currentConversationId,
senderType: 2,
senderId: currentCustomerId,
senderName: currentCustomerName,
contentType: 2,
content: imageUrl,
createTime: new Date()
};
if (!messageCache[currentConversationId]) {
messageCache[currentConversationId] = [];
}
messageCache[currentConversationId].push(newMsg);
renderMessages(messageCache[currentConversationId]);
}
}
});
}
// 客服在线状态
var isCustomerOnline = false;
// 切换客服在线状态
window.toggleCustomerStatus = function() {
var newStatus = isCustomerOnline ? 0 : 1;
admin.req({
method: "post",
contentType: "application/json;charset=UTF-8",
headers: {
'token': layui.data(setter.tableName)[setter.request.tokenName]
},
url: customerApiUrl + "customerapi/customer/status",
data: JSON.stringify({
userId: currentCustomerId,
status: newStatus
}),
done: function (res) {
if (res.code === 0) {
isCustomerOnline = !isCustomerOnline;
updateStatusDisplay();
layer.msg(isCustomerOnline ? '已上线' : '已离线');
// 如果上线了,重新加载会话列表
if (isCustomerOnline) {
loadConversations();
}
} else {
layer.msg(res.info || '操作失败');
}
}
});
};
// 更新状态显示
function updateStatusDisplay() {
var btn = document.getElementById('status-btn');
var display = document.getElementById('status-display');
if (!btn || !display) return;
if (isCustomerOnline) {
btn.textContent = '设为离线';
btn.className = 'layui-btn layui-btn-xs layui-btn-danger';
display.textContent = '在线';
display.className = 'layui-badge layui-bg-green';
} else {
btn.textContent = '设为在线';
btn.className = 'layui-btn layui-btn-xs';
display.textContent = '离线';
display.className = 'layui-badge';
}
}
// 更新顶部未读消息总数
function updateOnlineCount() {
var totalUnread = 0;
conversationList.forEach(function(conv) {
if (!readConversations.has(conv.id)) {
totalUnread += (unreadCounts[conv.id] || 0);
}
});
var countEl = document.getElementById('online-count');
if (countEl) {
countEl.textContent = totalUnread;
countEl.style.display = totalUnread > 0 ? '' : 'none';
}
}
// 检查当前客服在线状态
function checkCustomerStatus() {
if (!currentCustomerId) {
isCustomerOnline = false;
updateStatusDisplay();
return;
}
admin.req({
method: "get",
url: customerApiUrl + "customerapi/customer/online",
headers: {
'token': layui.data(setter.tableName)[setter.request.tokenName]
},
done: function (res) {
if (res.code === 0 && res.data) {
var onlineIds = res.data.map(function(c) { return c.userId; });
isCustomerOnline = onlineIds.indexOf(currentCustomerId) !== -1;
} else {
isCustomerOnline = false;
}
updateStatusDisplay();
}
});
}
// SignalR连接实例
var signalRConnection = null;
var signalRCore = null; // SignalR核心对象
// 初始化SignalR连接
function initSignalR() {
signalRCore = window.signalR || layui.signalR;
if (!signalRCore) {
console.warn('SignalR not loaded, trying to load...');
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@microsoft/signalr@6.0.0/dist/browser/signalr.min.js';
script.onload = function() {
console.log('SignalR loaded, starting connection...');
signalRCore = window.signalR;
startSignalRConnection();
};
script.onerror = function() {
console.error('SignalR load failed');
};
document.head.appendChild(script);
return;
}
startSignalRConnection();
}
function startSignalRConnection() {
if (!signalRCore) {
console.error('signalRCore is null');
return;
}
console.log('初始化SignalR连接,Hub地址:', customerApiUrl + "customerHub");
signalRConnection = new signalRCore.HubConnectionBuilder()
.withUrl(customerApiUrl + "customerHub")
.withHubProtocol(new signalRCore.JsonHubProtocol())
.withAutomaticReconnect()
.build();
// 监听消息接收
signalRConnection.on("OnMessageReceived", function(data) {
console.log("收到消息:", data);
var msgConvId = data.conversationId || data.ConversationId;
if (!messageCache[msgConvId]) {
messageCache[msgConvId] = [];
}
messageCache[msgConvId].push(data);
// 如果不是当前会话,增加未读计数
if (msgConvId !== currentConversationId) {
unreadCounts[msgConvId] = (unreadCounts[msgConvId] || 0) + 1;
}
// 更新顶部未读总数
updateOnlineCount();
// 如果是当前会话,刷新消息列表
if (currentConversationId === msgConvId) {
console.log("是当前会话,刷新消息列表");
renderMessages(messageCache[msgConvId]);
}
playNotificationSound();
});
// 监听新会话通知
signalRConnection.on("OnNewConversation", function(data) {
console.log("收到新会话通知:", data);
// 如果是分配给自己的会话,刷新会话列表
if (data.customerId === currentCustomerId) {
layer.msg('有新用户发起会话: ' + (data.userName || '未知用户'), {icon: 1, time: 3000});
loadConversations();
}
});
// 启动连接
signalRConnection.start().then(function() {
console.log("SignalR connected");
// 加入自己客服的队列
if (currentCustomerId) {
signalRConnection.invoke("JoinCustomerQueue", currentCustomerId)
.then(function() {
console.log("已加入客服队列:", currentCustomerId);
})
.catch(function(err) {
console.error("加入客服队列失败:", err);
});
}
conversationList.forEach(function(conv) {
if (conv.status === 1) {
signalRConnection.invoke("JoinConversation", conv.id);
}
});
}).catch(function(err) {
console.error("SignalR connection failed:", err);
});
}
// 通过SignalR发送消息
function sendMessageViaSignalR(conversationId, senderType, senderId, senderName, content) {
if (!signalRConnection || signalRConnection.state !== signalR.HubConnectionState.Connected) {
return false;
}
signalRConnection.invoke('SendMessage', conversationId, senderType, senderId, senderName, null, 1, content,"")
.then(function() {
console.log('SignalR消息发送成功');
return true;
})
.catch(function(err) {
console.error('SignalR发送失败:', err);
return false;
});
return true;
}
// 播放提示音
function playNotificationSound() {
try {
var audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2teleA4AQu3W459MBABQrtXpr3YRAFHI');
audio.volume = 0.3;
audio.play().catch(function() {});
} catch(e) {}
}
// 格式化时间
function formatTime(timestamp) {
if (!timestamp) return '';
var date;
if (typeof timestamp === 'string') {
date = new Date(timestamp);
} else {
date = new Date(timestamp);
}
// 转换为北京时间 (UTC+8)
var utc = date.getTime() + (date.getTimezoneOffset() * 60000);
var beijingDate = new Date(utc + (3600000 * 8));
var now = new Date();
var nowUtc = now.getTime() + (now.getTimezoneOffset() * 60000);
var nowBeijing = new Date(nowUtc + (3600000 * 8));
var diff = nowBeijing - beijingDate;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
return (beijingDate.getMonth() + 1) + '-' + beijingDate.getDate();
}
// 绑定事件
document.getElementById('send-btn').addEventListener('click', sendMessage);
document.getElementById('message-input').addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') {
sendMessage();
}
});
// 初始化
initCustomerInfo();
initSignalR();
// 定时轮询刷新会话列表(30秒间隔,SignalR已负责实时推送,轮询仅做兜底)
setInterval(function() {
if (currentCustomerId) {
loadConversations();
}
}, 30000);
});
</script>