1191 lines
33 KiB
HTML
Executable File
1191 lines
33 KiB
HTML
Executable File
{include file="public/header" /}
|
|
<style>
|
|
/* 客服工作台整体样式 */
|
|
.chat-workbench {
|
|
display: flex;
|
|
height: calc(100vh - 60px);
|
|
background: #f5f5f5;
|
|
gap: 10px;
|
|
padding: 10px;
|
|
}
|
|
|
|
/* 左侧会话列表 */
|
|
.chat-sessions {
|
|
width: 280px;
|
|
min-width: 280px;
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
|
}
|
|
.sessions-header {
|
|
padding: 15px;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.sessions-header h3 {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
.sessions-tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
}
|
|
.sessions-tabs .tab-item {
|
|
flex: 1;
|
|
padding: 12px 0;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
color: #666;
|
|
border-bottom: 2px solid transparent;
|
|
transition: all 0.3s;
|
|
}
|
|
.sessions-tabs .tab-item:hover {
|
|
color: #009688;
|
|
}
|
|
.sessions-tabs .tab-item.active {
|
|
color: #009688;
|
|
border-bottom-color: #009688;
|
|
}
|
|
.sessions-tabs .tab-item .badge {
|
|
display: inline-block;
|
|
min-width: 18px;
|
|
height: 18px;
|
|
line-height: 18px;
|
|
padding: 0 5px;
|
|
font-size: 11px;
|
|
background: #ff5722;
|
|
color: #fff;
|
|
border-radius: 9px;
|
|
margin-left: 4px;
|
|
}
|
|
.sessions-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
.session-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 15px;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid #f5f5f5;
|
|
transition: background 0.2s;
|
|
}
|
|
.session-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
.session-item.active {
|
|
background: #e8f5e9;
|
|
border-left: 3px solid #009688;
|
|
}
|
|
.session-avatar {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #009688, #4db6ac);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
margin-right: 12px;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
}
|
|
.session-avatar .online-dot {
|
|
position: absolute;
|
|
bottom: 2px;
|
|
right: 2px;
|
|
width: 10px;
|
|
height: 10px;
|
|
background: #4caf50;
|
|
border: 2px solid #fff;
|
|
border-radius: 50%;
|
|
}
|
|
.session-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.session-info .name-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 4px;
|
|
}
|
|
.session-info .username {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.session-info .time {
|
|
font-size: 11px;
|
|
color: #999;
|
|
flex-shrink: 0;
|
|
margin-left: 8px;
|
|
}
|
|
.session-info .last-msg {
|
|
font-size: 12px;
|
|
color: #999;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.session-badge {
|
|
min-width: 20px;
|
|
height: 20px;
|
|
line-height: 20px;
|
|
padding: 0 6px;
|
|
font-size: 11px;
|
|
background: #ff5722;
|
|
color: #fff;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
margin-left: 8px;
|
|
}
|
|
.sessions-empty {
|
|
padding: 40px 20px;
|
|
text-align: center;
|
|
color: #999;
|
|
}
|
|
.sessions-empty i {
|
|
font-size: 48px;
|
|
color: #ddd;
|
|
margin-bottom: 10px;
|
|
}
|
|
.accept-btn {
|
|
padding: 4px 10px;
|
|
background: #009688;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
margin-left: 8px;
|
|
transition: background 0.2s;
|
|
}
|
|
.accept-btn:hover {
|
|
background: #00796b;
|
|
}
|
|
|
|
/* 中间聊天区域 */
|
|
.chat-main {
|
|
flex: 1;
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
|
min-width: 0;
|
|
}
|
|
.chat-header {
|
|
padding: 15px 20px;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.chat-header .chat-title {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.chat-header .chat-title h3 {
|
|
margin: 0;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
.chat-header .chat-status {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-left: 15px;
|
|
font-size: 12px;
|
|
}
|
|
.chat-header .chat-status .status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
margin-right: 5px;
|
|
}
|
|
.chat-header .chat-status .status-dot.online {
|
|
background: #4caf50;
|
|
}
|
|
.chat-header .chat-status .status-dot.offline {
|
|
background: #9e9e9e;
|
|
}
|
|
.ws-status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
font-size: 12px;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
background: #ffebee;
|
|
color: #e53935;
|
|
}
|
|
.ws-status.connected {
|
|
background: #e8f5e9;
|
|
color: #43a047;
|
|
}
|
|
.ws-status i {
|
|
margin-right: 4px;
|
|
}
|
|
|
|
/* 消息区域 */
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
background: #f5f5f5;
|
|
}
|
|
.message-placeholder {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #999;
|
|
}
|
|
.message-placeholder i {
|
|
font-size: 64px;
|
|
color: #ddd;
|
|
margin-bottom: 15px;
|
|
}
|
|
.message-placeholder p {
|
|
font-size: 14px;
|
|
}
|
|
.message-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
.message-date {
|
|
text-align: center;
|
|
margin: 15px 0;
|
|
}
|
|
.message-date span {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
background: rgba(0,0,0,0.1);
|
|
border-radius: 12px;
|
|
font-size: 11px;
|
|
color: #666;
|
|
}
|
|
.message-row {
|
|
display: flex;
|
|
margin-bottom: 12px;
|
|
}
|
|
.message-row.user {
|
|
justify-content: flex-start;
|
|
}
|
|
.message-row.agent {
|
|
justify-content: flex-end;
|
|
}
|
|
.message-avatar {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
background: #009688;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
font-size: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
.message-row.user .message-avatar {
|
|
margin-right: 10px;
|
|
background: #607d8b;
|
|
}
|
|
.message-row.agent .message-avatar {
|
|
margin-left: 10px;
|
|
order: 2;
|
|
}
|
|
.message-content {
|
|
max-width: 60%;
|
|
}
|
|
.message-bubble {
|
|
padding: 10px 14px;
|
|
border-radius: 12px;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
word-break: break-word;
|
|
}
|
|
.message-row.user .message-bubble {
|
|
background: #fff;
|
|
color: #333;
|
|
border-top-left-radius: 4px;
|
|
}
|
|
.message-row.agent .message-bubble {
|
|
background: #009688;
|
|
color: #fff;
|
|
border-top-right-radius: 4px;
|
|
}
|
|
.message-bubble img {
|
|
max-width: 200px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
}
|
|
.message-time {
|
|
font-size: 11px;
|
|
color: #999;
|
|
margin-top: 4px;
|
|
}
|
|
.message-row.agent .message-time {
|
|
text-align: right;
|
|
}
|
|
.typing-indicator {
|
|
display: none;
|
|
padding: 10px 20px;
|
|
color: #999;
|
|
font-size: 12px;
|
|
}
|
|
.typing-indicator.show {
|
|
display: block;
|
|
}
|
|
|
|
/* 输入区域 */
|
|
.chat-input-area {
|
|
border-top: 1px solid #e8e8e8;
|
|
background: #fff;
|
|
}
|
|
.input-toolbar {
|
|
padding: 8px 15px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
.input-toolbar .tool-btn {
|
|
height: 28px;
|
|
padding: 0 12px;
|
|
border: 1px solid #e8e8e8;
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #666;
|
|
font-size: 12px;
|
|
transition: all 0.2s;
|
|
}
|
|
.input-toolbar .tool-btn:hover:not(:disabled) {
|
|
background: #f5f5f5;
|
|
color: #009688;
|
|
border-color: #009688;
|
|
}
|
|
.input-toolbar .tool-btn:disabled {
|
|
color: #ccc;
|
|
cursor: not-allowed;
|
|
border-color: #eee;
|
|
}
|
|
.input-wrapper {
|
|
padding: 10px 15px;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 10px;
|
|
}
|
|
.input-wrapper textarea {
|
|
flex: 1;
|
|
border: 1px solid #e8e8e8;
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
font-size: 14px;
|
|
resize: none;
|
|
outline: none;
|
|
min-height: 60px;
|
|
max-height: 120px;
|
|
transition: border-color 0.2s;
|
|
}
|
|
.input-wrapper textarea:focus {
|
|
border-color: #009688;
|
|
}
|
|
.input-wrapper textarea:disabled {
|
|
background: #f5f5f5;
|
|
}
|
|
.send-btn {
|
|
padding: 10px 24px;
|
|
background: #009688;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
white-space: nowrap;
|
|
}
|
|
.send-btn:hover:not(:disabled) {
|
|
background: #00796b;
|
|
}
|
|
.send-btn:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* 快捷回复面板 */
|
|
.quick-reply-panel {
|
|
display: none;
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: #fff;
|
|
border: 1px solid #e8e8e8;
|
|
border-radius: 8px 8px 0 0;
|
|
box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
.quick-reply-panel.show {
|
|
display: block;
|
|
}
|
|
.quick-reply-item {
|
|
padding: 10px 15px;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid #f5f5f5;
|
|
transition: background 0.2s;
|
|
}
|
|
.quick-reply-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
.quick-reply-item .title {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
margin-bottom: 3px;
|
|
}
|
|
.quick-reply-item .content {
|
|
font-size: 12px;
|
|
color: #999;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* 右侧用户信息面板 */
|
|
.chat-user-panel {
|
|
width: 260px;
|
|
min-width: 260px;
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
display: none;
|
|
flex-direction: column;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
|
}
|
|
.chat-user-panel.show {
|
|
display: flex;
|
|
}
|
|
.user-profile {
|
|
padding: 25px 20px;
|
|
text-align: center;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
}
|
|
.user-profile .avatar {
|
|
width: 72px;
|
|
height: 72px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #009688, #4db6ac);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
font-size: 28px;
|
|
font-weight: 600;
|
|
margin: 0 auto 12px;
|
|
}
|
|
.user-profile .username {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 5px;
|
|
}
|
|
.user-profile .source-tag {
|
|
display: inline-block;
|
|
padding: 3px 10px;
|
|
background: #e8f5e9;
|
|
color: #009688;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
}
|
|
.user-details {
|
|
flex: 1;
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
}
|
|
.detail-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid #f5f5f5;
|
|
}
|
|
.detail-item .label {
|
|
font-size: 13px;
|
|
color: #999;
|
|
}
|
|
.detail-item .value {
|
|
font-size: 13px;
|
|
color: #333;
|
|
font-weight: 500;
|
|
}
|
|
.detail-item .value.money {
|
|
color: #ff9800;
|
|
}
|
|
.user-actions {
|
|
padding: 15px;
|
|
border-top: 1px solid #e8e8e8;
|
|
}
|
|
.user-actions .action-btn {
|
|
width: 100%;
|
|
padding: 10px;
|
|
margin-bottom: 8px;
|
|
border: 1px solid #e8e8e8;
|
|
background: #fff;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.user-actions .action-btn i {
|
|
margin-right: 6px;
|
|
}
|
|
.user-actions .action-btn:hover {
|
|
border-color: #009688;
|
|
color: #009688;
|
|
}
|
|
.user-actions .action-btn.danger {
|
|
color: #e53935;
|
|
}
|
|
.user-actions .action-btn.danger:hover {
|
|
border-color: #e53935;
|
|
background: #ffebee;
|
|
}
|
|
</style>
|
|
<body>
|
|
<div class="chat-workbench">
|
|
<!-- 左侧会话列表 -->
|
|
<div class="chat-sessions">
|
|
<div class="sessions-header">
|
|
<h3>会话列表</h3>
|
|
<button class="layui-btn layui-btn-xs layui-btn-primary" id="refresh-sessions" title="刷新">刷新</button>
|
|
</div>
|
|
<div class="sessions-tabs">
|
|
<div class="tab-item active" data-status="1">
|
|
进行中 <span class="badge" id="active-count">0</span>
|
|
</div>
|
|
<div class="tab-item" data-status="0">
|
|
待接入 <span class="badge" id="pending-count">0</span>
|
|
</div>
|
|
</div>
|
|
<div class="sessions-list">
|
|
<div id="session-list"></div>
|
|
<div id="pending-list" style="display:none;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 中间聊天窗口 -->
|
|
<div class="chat-main">
|
|
<div class="chat-header">
|
|
<div class="chat-title">
|
|
<h3 id="chat-title">请选择会话</h3>
|
|
<div class="chat-status" id="user-status" style="display:none;">
|
|
<span class="status-dot online"></span>
|
|
<span>在线</span>
|
|
</div>
|
|
</div>
|
|
<div class="chat-header-actions">
|
|
<span class="ws-status" id="ws-status">未连接</span>
|
|
<button class="layui-btn layui-btn-xs layui-btn-danger" id="end-session" style="display:none;margin-left:10px;">结束会话</button>
|
|
</div>
|
|
</div>
|
|
<div class="chat-messages" id="message-area">
|
|
<div class="message-placeholder">
|
|
<div style="font-size:48px;color:#ddd;margin-bottom:15px;">💬</div>
|
|
<p>请从左侧选择一个会话开始聊天</p>
|
|
</div>
|
|
</div>
|
|
<div class="typing-indicator" id="typing-indicator">
|
|
用户正在输入...
|
|
</div>
|
|
<div class="chat-input-area" style="position:relative;">
|
|
<div class="quick-reply-panel" id="quick-reply-panel"></div>
|
|
<div class="input-toolbar">
|
|
<button class="tool-btn" id="show-quick-reply" title="快捷回复" disabled>快捷</button>
|
|
</div>
|
|
<div class="input-wrapper">
|
|
<textarea id="message-input" placeholder="输入消息,按 Enter 发送..." disabled></textarea>
|
|
<button class="send-btn" id="send-message" disabled>发送</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧用户信息面板 -->
|
|
<div class="chat-user-panel" id="user-panel">
|
|
<div class="user-profile">
|
|
<div class="avatar" id="user-avatar">U</div>
|
|
<div class="username" id="user-username">-</div>
|
|
<span class="source-tag" id="user-source">-</span>
|
|
</div>
|
|
<div class="user-details">
|
|
<div class="detail-item">
|
|
<span class="label">账户余额</span>
|
|
<span class="value money" id="user-money">-</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">会话ID</span>
|
|
<span class="value" id="session-id">-</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">会话时长</span>
|
|
<span class="value" id="session-duration">-</span>
|
|
</div>
|
|
</div>
|
|
<div class="user-actions">
|
|
<button class="action-btn" id="view-user-detail">查看用户详情</button>
|
|
<button class="action-btn" id="view-chat-history">历史消息记录</button>
|
|
<button class="action-btn danger" id="end-session-panel" style="display:none;">结束会话</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.5.0/socket.io.min.js"></script>
|
|
<script>
|
|
layui.use(['layer', 'upload'], function(){
|
|
var layer = layui.layer;
|
|
var upload = layui.upload;
|
|
|
|
var adminId = {$admin_id};
|
|
var loginToken = '{$login_token}';
|
|
var currentSessionId = null;
|
|
var currentSession = null;
|
|
var socket = null;
|
|
var currentTab = 1; // 1=进行中, 0=待接入
|
|
var sessionStartTime = null;
|
|
var durationTimer = null;
|
|
|
|
// WebSocket连接
|
|
function connectWebSocket() {
|
|
console.log('开始连接WebSocket...');
|
|
socket = io('wss://api.g7g7.top', {
|
|
transports: ['websocket'],
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
reconnectionAttempts: 5
|
|
});
|
|
|
|
socket.on('connect', function() {
|
|
console.log('Socket.IO connect 事件触发, socket.id:', socket.id);
|
|
updateWsStatus(true);
|
|
socket.emit('chat.connect', {
|
|
token: loginToken,
|
|
role: 'agent',
|
|
maxSessions: {$admin_status.max_sessions}
|
|
});
|
|
console.log('已发送 chat.connect 事件');
|
|
});
|
|
|
|
socket.on('disconnect', function(reason) {
|
|
console.log('Socket.IO disconnect 事件触发, 原因:', reason);
|
|
updateWsStatus(false);
|
|
});
|
|
|
|
socket.on('connect_error', function(error) {
|
|
console.log('Socket.IO connect_error:', error);
|
|
updateWsStatus(false);
|
|
});
|
|
|
|
socket.on('chat_connected', function(data) {
|
|
if (data.success) {
|
|
layer.msg('连接成功', {icon: 1, time: 1500});
|
|
loadSessions();
|
|
loadPendingSessions();
|
|
} else {
|
|
layer.msg('连接失败: ' + data.error, {icon: 2});
|
|
}
|
|
});
|
|
|
|
socket.on('chat_message_new', function(data) {
|
|
if (data.sessionId == currentSessionId) {
|
|
appendMessage(data, 'user');
|
|
playNotificationSound();
|
|
} else {
|
|
playNotificationSound();
|
|
}
|
|
loadSessions();
|
|
});
|
|
|
|
socket.on('chat_session_new', function(data) {
|
|
layer.msg('新会话分配', {icon: 1});
|
|
playNotificationSound();
|
|
loadSessions();
|
|
});
|
|
|
|
socket.on('chat_session_ended', function(data) {
|
|
if (data.sessionId == currentSessionId) {
|
|
layer.msg('会话已结束', {icon: 0});
|
|
closeCurrentSession();
|
|
}
|
|
loadSessions();
|
|
});
|
|
|
|
socket.on('chat_user_typing', function(data) {
|
|
if (data.sessionId == currentSessionId) {
|
|
showTypingIndicator();
|
|
}
|
|
});
|
|
|
|
// 心跳
|
|
setInterval(function() {
|
|
if (socket && socket.connected) {
|
|
socket.emit('chat.ping', {});
|
|
}
|
|
}, 25000);
|
|
}
|
|
|
|
// 更新WebSocket状态显示
|
|
function updateWsStatus(connected) {
|
|
var $status = $('#ws-status');
|
|
if (connected) {
|
|
$status.addClass('connected').text('已连接');
|
|
} else {
|
|
$status.removeClass('connected').text('未连接');
|
|
}
|
|
}
|
|
|
|
// 播放提示音
|
|
function playNotificationSound() {
|
|
// 可以添加提示音逻辑
|
|
}
|
|
|
|
// 显示打字指示器
|
|
function showTypingIndicator() {
|
|
var $indicator = $('#typing-indicator');
|
|
$indicator.addClass('show');
|
|
setTimeout(function() {
|
|
$indicator.removeClass('show');
|
|
}, 3000);
|
|
}
|
|
|
|
// 加载会话列表
|
|
function loadSessions() {
|
|
$.get('/chat/sessions', {status: 1}, function(res) {
|
|
if (res.code === 0) {
|
|
$('#active-count').text(res.data.length);
|
|
renderSessionList(res.data, '#session-list', false);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 加载待分配会话
|
|
function loadPendingSessions() {
|
|
$.get('/chat/pending', function(res) {
|
|
if (res.code === 0) {
|
|
$('#pending-count').text(res.data.length);
|
|
renderSessionList(res.data, '#pending-list', true);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 渲染会话列表
|
|
function renderSessionList(sessions, container, isPending) {
|
|
var html = '';
|
|
if (sessions.length === 0) {
|
|
html = '<div class="sessions-empty">';
|
|
html += '<div style="font-size:36px;color:#ddd;margin-bottom:10px;">💬</div>';
|
|
html += '<p>暂无会话</p>';
|
|
html += '</div>';
|
|
} else {
|
|
sessions.forEach(function(session) {
|
|
var isActive = session.id == currentSessionId ? ' active' : '';
|
|
var username = session.user_info ? session.user_info.username : '未知用户';
|
|
var firstChar = username.charAt(0).toUpperCase();
|
|
var unreadBadge = session.unread_count > 0 ? '<span class="session-badge">' + session.unread_count + '</span>' : '';
|
|
var lastMsg = session.last_msg || '暂无消息';
|
|
if (lastMsg.length > 20) lastMsg = lastMsg.substring(0, 20) + '...';
|
|
|
|
html += '<div class="session-item' + isActive + '" data-id="' + session.id + '">';
|
|
html += ' <div class="session-avatar">' + firstChar + '<span class="online-dot"></span></div>';
|
|
html += ' <div class="session-info">';
|
|
html += ' <div class="name-row">';
|
|
html += ' <span class="username">' + username + '</span>';
|
|
html += ' <span class="time">' + (session.last_msg_time_fmt || session.create_time_fmt || '') + '</span>';
|
|
html += ' </div>';
|
|
html += ' <div class="last-msg">' + lastMsg + '</div>';
|
|
html += ' </div>';
|
|
if (isPending) {
|
|
html += ' <button class="accept-btn" data-id="' + session.id + '">接入</button>';
|
|
} else {
|
|
html += unreadBadge;
|
|
}
|
|
html += '</div>';
|
|
});
|
|
}
|
|
$(container).html(html);
|
|
|
|
// 绑定点击事件
|
|
$(container + ' .session-item').click(function(e) {
|
|
// 如果点击的是接入按钮,不触发打开会话
|
|
if ($(e.target).hasClass('accept-btn')) return;
|
|
var sessionId = $(this).data('id');
|
|
if (!isPending) {
|
|
openSession(sessionId);
|
|
}
|
|
});
|
|
|
|
// 绑定接入按钮事件
|
|
if (isPending) {
|
|
$(container + ' .accept-btn').click(function(e) {
|
|
e.stopPropagation();
|
|
var sessionId = $(this).data('id');
|
|
acceptSession(sessionId);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 接入会话
|
|
function acceptSession(sessionId) {
|
|
$.post('/chat/acceptSession', {session_id: sessionId}, function(res) {
|
|
if (res.code === 0) {
|
|
layer.msg('接入成功', {icon: 1, time: 1500});
|
|
loadSessions();
|
|
loadPendingSessions();
|
|
// 自动打开刚接入的会话
|
|
setTimeout(function() {
|
|
openSession(sessionId);
|
|
// 切换到进行中标签
|
|
$('.sessions-tabs .tab-item[data-status="1"]').click();
|
|
}, 500);
|
|
} else {
|
|
layer.msg(res.msg || '接入失败', {icon: 2});
|
|
loadPendingSessions();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 打开会话
|
|
function openSession(sessionId) {
|
|
// 移除之前的选中状态
|
|
$('.session-item').removeClass('active');
|
|
$('.session-item[data-id="' + sessionId + '"]').addClass('active');
|
|
|
|
currentSessionId = sessionId;
|
|
|
|
// 加载消息
|
|
$.get('/chat/messages', {session_id: sessionId}, function(res) {
|
|
if (res.code === 0) {
|
|
$('#message-area').html('');
|
|
res.data.forEach(function(msg) {
|
|
appendMessage(msg, msg.sender_type === 1 ? 'user' : 'agent');
|
|
});
|
|
scrollToBottom();
|
|
}
|
|
});
|
|
|
|
// 获取会话详情
|
|
$.get('/chat/sessions', {status: 1}, function(res) {
|
|
if (res.code === 0) {
|
|
var session = res.data.find(function(s) { return s.id == sessionId; });
|
|
if (session) {
|
|
currentSession = session;
|
|
var username = session.user_info ? session.user_info.username : '未知用户';
|
|
var money = session.user_info ? session.user_info.money : '-';
|
|
var sourceName = session.source_name || '-';
|
|
var firstChar = username.charAt(0).toUpperCase();
|
|
|
|
// 更新聊天头部
|
|
$('#chat-title').text(username);
|
|
$('#user-status').show();
|
|
$('#end-session').show();
|
|
|
|
// 更新右侧用户面板
|
|
$('#user-avatar').text(firstChar);
|
|
$('#user-username').text(username);
|
|
$('#user-money').text(money);
|
|
$('#user-source').text(sourceName);
|
|
$('#session-id').text(session.id);
|
|
$('#user-panel').addClass('show');
|
|
$('#end-session-panel').show();
|
|
|
|
// 启用输入
|
|
$('#message-input, #send-message, #show-quick-reply').prop('disabled', false);
|
|
|
|
// 开始计时
|
|
startDurationTimer(session.create_time);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 会话时长计时器
|
|
function startDurationTimer(startTime) {
|
|
if (durationTimer) clearInterval(durationTimer);
|
|
// startTime 是 Unix 时间戳(秒),需要转换为毫秒
|
|
sessionStartTime = startTime * 1000;
|
|
|
|
function updateDuration() {
|
|
var now = Date.now();
|
|
var diff = Math.floor((now - sessionStartTime) / 1000);
|
|
var hours = Math.floor(diff / 3600);
|
|
var minutes = Math.floor((diff % 3600) / 60);
|
|
var seconds = diff % 60;
|
|
var str = '';
|
|
if (hours > 0) str += hours + '时';
|
|
str += minutes + '分' + seconds + '秒';
|
|
$('#session-duration').text(str);
|
|
}
|
|
|
|
updateDuration();
|
|
durationTimer = setInterval(updateDuration, 1000);
|
|
}
|
|
|
|
// 追加消息
|
|
function appendMessage(msg, type) {
|
|
var content = msg.content || (msg.data ? msg.data.content : '');
|
|
var time = msg.create_time_fmt || new Date().toLocaleTimeString();
|
|
var isImage = msg.msg_type === 2 || (msg.data && msg.data.msgType === 2);
|
|
var username = type === 'user' ? (currentSession && currentSession.user_info ? currentSession.user_info.username : 'U') : '客服';
|
|
var firstChar = username.charAt(0).toUpperCase();
|
|
|
|
var html = '<div class="message-row ' + type + '">';
|
|
html += ' <div class="message-avatar">' + firstChar + '</div>';
|
|
html += ' <div class="message-content">';
|
|
html += ' <div class="message-bubble">';
|
|
if (isImage) {
|
|
html += '<img src="' + content + '" onclick="layer.photos({photos: {data: [{src: \'' + content + '\'}]}})">';
|
|
} else {
|
|
html += escapeHtml(content);
|
|
}
|
|
html += ' </div>';
|
|
html += ' <div class="message-time">' + time + '</div>';
|
|
html += ' </div>';
|
|
html += '</div>';
|
|
|
|
$('#message-area').append(html);
|
|
scrollToBottom();
|
|
}
|
|
|
|
// HTML转义
|
|
function escapeHtml(text) {
|
|
var div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// 滚动到底部
|
|
function scrollToBottom() {
|
|
var area = document.getElementById('message-area');
|
|
area.scrollTop = area.scrollHeight;
|
|
}
|
|
|
|
// 发送消息
|
|
$('#send-message').click(function() {
|
|
sendMessage();
|
|
});
|
|
|
|
function sendMessage() {
|
|
var content = $('#message-input').val().trim();
|
|
if (!content) {
|
|
layer.msg('请输入消息', {icon: 0});
|
|
return;
|
|
}
|
|
if (!currentSessionId) {
|
|
layer.msg('请先选择会话', {icon: 0});
|
|
return;
|
|
}
|
|
|
|
socket.emit('chat.message.send', {
|
|
sessionId: currentSessionId,
|
|
msgType: 1,
|
|
content: content,
|
|
clientMsgId: Date.now()
|
|
});
|
|
|
|
appendMessage({content: content, create_time_fmt: new Date().toLocaleTimeString()}, 'agent');
|
|
$('#message-input').val('');
|
|
}
|
|
|
|
// 回车发送
|
|
$('#message-input').keypress(function(e) {
|
|
if (e.which === 13 && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
// 结束会话
|
|
function endCurrentSession() {
|
|
if (!currentSessionId) return;
|
|
layer.confirm('确定结束此会话?', {icon: 3, title: '提示'}, function(index) {
|
|
$.post('/chat/endSession', {session_id: currentSessionId}, function(res) {
|
|
if (res.code === 0) {
|
|
layer.msg('会话已结束', {icon: 1});
|
|
closeCurrentSession();
|
|
loadSessions();
|
|
} else {
|
|
layer.msg(res.msg || '操作失败', {icon: 2});
|
|
}
|
|
});
|
|
layer.close(index);
|
|
});
|
|
}
|
|
|
|
$('#end-session, #end-session-panel').click(endCurrentSession);
|
|
|
|
// 关闭当前会话
|
|
function closeCurrentSession() {
|
|
currentSessionId = null;
|
|
currentSession = null;
|
|
if (durationTimer) clearInterval(durationTimer);
|
|
|
|
$('#chat-title').text('请选择会话');
|
|
$('#user-status').hide();
|
|
$('#end-session').hide();
|
|
$('#user-panel').removeClass('show');
|
|
$('#end-session-panel').hide();
|
|
$('#message-input, #send-message, #show-quick-reply').prop('disabled', true);
|
|
$('#message-area').html('<div class="message-placeholder"><div style="font-size:48px;color:#ddd;margin-bottom:15px;">💬</div><p>请从左侧选择一个会话开始聊天</p></div>');
|
|
|
|
$('.session-item').removeClass('active');
|
|
}
|
|
|
|
// 快捷回复
|
|
var quickReplyLoaded = false;
|
|
var quickReplies = [];
|
|
|
|
function loadQuickReplies() {
|
|
$.get('/chat/quickReplies', function(res) {
|
|
if (res.code === 0) {
|
|
quickReplies = res.data;
|
|
quickReplyLoaded = true;
|
|
renderQuickReplyPanel();
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderQuickReplyPanel() {
|
|
var html = '';
|
|
quickReplies.forEach(function(item) {
|
|
html += '<div class="quick-reply-item" data-content="' + escapeHtml(item.content) + '">';
|
|
html += ' <div class="title">' + escapeHtml(item.title) + '</div>';
|
|
html += ' <div class="content">' + escapeHtml(item.content.substring(0, 50)) + (item.content.length > 50 ? '...' : '') + '</div>';
|
|
html += '</div>';
|
|
});
|
|
$('#quick-reply-panel').html(html);
|
|
|
|
$('#quick-reply-panel .quick-reply-item').click(function() {
|
|
var content = $(this).data('content');
|
|
$('#message-input').val(content);
|
|
$('#quick-reply-panel').removeClass('show');
|
|
$('#message-input').focus();
|
|
});
|
|
}
|
|
|
|
$('#show-quick-reply').click(function() {
|
|
if (!quickReplyLoaded) {
|
|
loadQuickReplies();
|
|
}
|
|
$('#quick-reply-panel').toggleClass('show');
|
|
});
|
|
|
|
// 点击其他地方关闭快捷回复面板
|
|
$(document).click(function(e) {
|
|
if (!$(e.target).closest('#show-quick-reply, #quick-reply-panel').length) {
|
|
$('#quick-reply-panel').removeClass('show');
|
|
}
|
|
});
|
|
|
|
// 刷新会话
|
|
$('#refresh-sessions').click(function() {
|
|
loadSessions();
|
|
loadPendingSessions();
|
|
layer.msg('已刷新', {icon: 1, time: 1000});
|
|
});
|
|
|
|
// 标签切换
|
|
$('.sessions-tabs .tab-item').click(function() {
|
|
var status = $(this).data('status');
|
|
currentTab = status;
|
|
|
|
$(this).addClass('active').siblings().removeClass('active');
|
|
|
|
if (status === 0) {
|
|
$('#session-list').hide();
|
|
$('#pending-list').show();
|
|
loadPendingSessions();
|
|
} else {
|
|
$('#pending-list').hide();
|
|
$('#session-list').show();
|
|
loadSessions();
|
|
}
|
|
});
|
|
|
|
// 图片上传
|
|
upload.render({
|
|
elem: '#upload-image',
|
|
url: '/chat/upload',
|
|
accept: 'images',
|
|
before: function() {
|
|
layer.load(1);
|
|
},
|
|
done: function(res) {
|
|
layer.closeAll('loading');
|
|
if (res.code === 0) {
|
|
socket.emit('chat.message.send', {
|
|
sessionId: currentSessionId,
|
|
msgType: 2,
|
|
content: res.url,
|
|
clientMsgId: Date.now()
|
|
});
|
|
appendMessage({content: res.url, msg_type: 2, create_time_fmt: new Date().toLocaleTimeString()}, 'agent');
|
|
} else {
|
|
layer.msg(res.msg || '上传失败', {icon: 2});
|
|
}
|
|
},
|
|
error: function() {
|
|
layer.closeAll('loading');
|
|
layer.msg('上传失败', {icon: 2});
|
|
}
|
|
});
|
|
|
|
// 查看用户详情
|
|
$('#view-user-detail').click(function() {
|
|
if (currentSession && currentSession.user_id) {
|
|
layer.open({
|
|
type: 2,
|
|
title: '用户详情',
|
|
area: ['800px', '600px'],
|
|
content: '/player/player_edit?id=' + currentSession.user_id
|
|
});
|
|
}
|
|
});
|
|
|
|
// 查看历史记录
|
|
$('#view-chat-history').click(function() {
|
|
if (currentSessionId) {
|
|
layer.open({
|
|
type: 2,
|
|
title: '历史消息记录',
|
|
area: ['800px', '600px'],
|
|
content: '/chat/record?session_id=' + currentSessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
// 初始化
|
|
connectWebSocket();
|
|
loadQuickReplies();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|